<?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\Migration\Encryption;

use Shopware\Components\Logger;
use Shopware\Components\Model\ModelManager;
use Shopware\Plugins\ViisonCommon\Components\HiddenConfigStorageService;
use Shopware\Plugins\ViisonShippingCommon\Classes\ConfigurationPropertyEncryption;
use Shopware\Plugins\ViisonShippingCommon\Classes\Exceptions\CryptographyException;

class RemoveEncryptionMigration
{
    const DEFAULT_PASSPHRASE = 'Vai6;enoek4a!l9Keig0a-0kL.iK49#Q';
    const ENCRYPTION_METHOD_FIELD_NAME = 'encryptionMethod';
    const ENCRYPTION_METHOD_MCRYPT = 'rijndael256';
    const ENCRYPTION_METHOD_OPENSSL = 'aes-256-cbc';
    const HIDDEN_CONFIG_ELEMENT_NAME = 'viison_encryption_removed_from_table';

    /**
     * @var \Zend_Db_Adapter_Abstract
     */
    private $db;

    /**
     * @var Logger $pluginLogger
     */
    private $pluginLogger;

    /**
     * @var HiddenConfigStorageService
     */
    private $hiddenConfigStorageService;

    /**
     * @param \Zend_Db_Adapter_Abstract $db
     * @param $pluginLogger
     * @param ModelManager $entityManager
     */
    public function __construct(\Zend_Db_Adapter_Abstract $db, $pluginLogger, ModelManager $entityManager)
    {
        $this->db = $db;
        $this->pluginLogger = $pluginLogger;
        $this->hiddenConfigStorageService = new HiddenConfigStorageService($entityManager);
    }

    /**
     * @param string $tableName
     * @param array $columnsToDecrypt
     * @param string $decryptionKey
     * @return bool
     */
    public function migrate($tableName, array $columnsToDecrypt, $decryptionKey = self::DEFAULT_PASSPHRASE)
    {
        if ($this->isMigrationCompletedForTable($tableName)) {
            return true;
        }

        $isDecryptionSuccessful = true;
        $selectAllRowsQuery = sprintf('SELECT * FROM %s', $this->db->quoteIdentifier($tableName));
        $rows = $this->db->fetchAll($selectAllRowsQuery);
        foreach ($rows as $row) {
            foreach ($columnsToDecrypt as $encryptedColumnName) {
                try {
                    $decryptedColumnValue = $this->decrypt(
                        $row[$encryptedColumnName],
                        $decryptionKey,
                        // If the column `encryptionMethod` doesn't exist then the Adapter is using mcrypt. (mcrypt or openSSL is only supported)
                        $row[self::ENCRYPTION_METHOD_FIELD_NAME] ?: self::ENCRYPTION_METHOD_MCRYPT
                    );
                } catch (CryptographyException $e) {
                    $isDecryptionSuccessful = false;

                    // In case if the field decryption has failed, the field value will be set to empty string
                    // and the old password (together with the error message, shop id, ...) will be saved in a
                    // log file so that in case of support we can decrypt it manually.
                    $decryptedColumnValue = '';

                    $message = sprintf(
                        'ViisonShippingCommon: Exception while decrypting the field %s (shop id %d)',
                        $encryptedColumnName,
                        $row['shopId']
                    );
                    $this->pluginLogger->error(
                        $message,
                        [
                            'tableName' => $tableName,
                            'shopId' => $row['shopId'],
                            'fieldName' => $encryptedColumnName,
                            'exceptionMessage' => $e->getMessage(),
                        ]
                    );
                }

                $updateQuery = sprintf(
                    'UPDATE %s SET %s = :decryptedColumnValue WHERE id = :id',
                    $this->db->quoteIdentifier($tableName),
                    $this->db->quoteIdentifier($encryptedColumnName)
                );

                $this->db->query(
                    $updateQuery,
                    [
                        'decryptedColumnValue' => $decryptedColumnValue,
                        'id' => $row['id'],
                    ]
                );
            }
        }
        $this->markTableAsMigrated($tableName);

        return $isDecryptionSuccessful;
    }

    /**
     * @param $tableName
     */
    public function uninstall($tableName)
    {
        $value = $this->hiddenConfigStorageService->getConfigValue(self::HIDDEN_CONFIG_ELEMENT_NAME);

        if (!$value) {
            return;
        }

        $tables = explode(',', $value);
        $tables = array_filter($tables, function ($table) use ($tableName) {
            return $tableName !== $table;
        });

        if (count($tables) === 0) {
            $this->hiddenConfigStorageService->removeConfigValue(self::HIDDEN_CONFIG_ELEMENT_NAME);
        } else {
            $this->hiddenConfigStorageService->setConfigValue(self::HIDDEN_CONFIG_ELEMENT_NAME, 'string', implode(',', $tables));
        }
    }

    /**
     * @param string $tableName
     * @return bool
     */
    private function isMigrationCompletedForTable($tableName)
    {
        $value = $this->hiddenConfigStorageService->getConfigValue(self::HIDDEN_CONFIG_ELEMENT_NAME);

        if ($value === null) {
            return false;
        }

        return in_array($tableName, explode(',', $value), true);
    }

    /**
     * @param string $encryptedValue
     * @param string $decryptionKey More or less every Adapter is using the same key.
     * @param null $encryptionMethod
     * @return string
     * @throws CryptographyException
     */
    private function decrypt($encryptedValue, $decryptionKey, $encryptionMethod)
    {
        if (!$encryptedValue) {
            return '';
        }

        if (($encryptionMethod === self::ENCRYPTION_METHOD_MCRYPT && !extension_loaded('mcrypt'))
            || ($encryptionMethod === self::ENCRYPTION_METHOD_OPENSSL && !extension_loaded('openssl'))) {
            throw new CryptographyException('Missing module mcrypt or openssl.');
        }

        // For mcrypt the value needs to be decoded first.
        $encryptedValue = base64_decode($encryptedValue);

        return ConfigurationPropertyEncryption::decrypt($encryptedValue, $decryptionKey, $encryptionMethod);
    }

    /**
     * After the migration is completed, we are adding a hidden config select box so that we can ensure idempotency.
     *
     * @param string $tableName
     */
    private function markTableAsMigrated($tableName)
    {
        $value = $this->hiddenConfigStorageService->getConfigValue(self::HIDDEN_CONFIG_ELEMENT_NAME);

        if ($value) {
            $value .= ',' . $tableName;
        } else {
            $value = $tableName;
        }

        $this->hiddenConfigStorageService->setConfigValue(self::HIDDEN_CONFIG_ELEMENT_NAME, 'string', $value);
    }
}
