<?php
/**
 * Created by PhpStorm.
 * User: enured
 * Date: 5/18/22
 * Time: 10:30 AM
 */

namespace IMATHUZH\Qfq\Core\Helper;


use CodeException;
use IMATHUZH\Qfq\Core\Database\Database;
use IMATHUZH\Qfq\Core\Store\Store;


/**
 * Class EncryptDecypt
 * @package qfq
 */
class EncryptDecrypt {

    /**
     * Builds a ciphertext from a plaintext string.
     *
     * Is encrypted with openssl and returns ciphertext.
     *
     * @param string $plaintext
     * @param string $cbcCipher
     * @param string $key
     * @param string $iv
     * @return string
     */
    private static function encrypt(string $plaintext, string $key, string $cbcCipher, string $iv): string {
        // Initialise variable
        $ciphertext = '';

        // Check if cipher method exists in openssl from php
        if (in_array($cbcCipher, openssl_get_cipher_methods())) {
            // Encrypt plaintext with given arguments and return
            $ciphertext = openssl_encrypt($plaintext, $cbcCipher, $key, $options = 0, $iv);
        }

        return $ciphertext;
    }

    /**
     * Decrypts an encrypted plaintext.
     *
     * Done with openssl and returns plaintext.
     *
     * @param string $ciphertext
     * @param string $cbcCipher
     * @param string $key
     * @param string $iv
     * @return string
     */
    private static function decrypt(string $ciphertext, string $key, string $cbcCipher, string $iv): string {
        // Initialise variable
        $plaintext = '';

        // Check if cipherMethod exists in openssl from php
        if (in_array($cbcCipher, openssl_get_cipher_methods())) {
            // Decrypt ciphertext with given arguments and return
            $plaintext = openssl_decrypt($ciphertext, $cbcCipher, $key, $options = 0, $iv);
        }

        return $plaintext;
    }

    /**
     * Builds complete value for sharing in database.
     *
     * Built value should look like this -> $encryptedData$:cipherMethod:iv:ciphertext
     *
     * @param string $plaintext
     * @param string $cipherMethod
     * @param string $key
     * @return string
     */
    public static function buildDatabaseValue(string $plaintext, string $key, string $cipherMethod): string {
        switch ($cipherMethod) {
            case ENCRYPTION_METHOD_AES_128:
                $cbcCipher = 'aes-128-cbc';
                break;
            case ENCRYPTION_METHOD_AES_256:
                $cbcCipher = 'aes-256-cbc';
                break;
            default:
                return false;
        }

        $iv = rand(1000000000000000, 9000000000000000);
        $ciphertext = self::encrypt($plaintext, $key, $cbcCipher, $iv);
        return ENCRYPTION_KEYWORD_ENCRYPTED_DATA . ':' . $cbcCipher . ':' . $iv . ':' . $ciphertext;
    }

    /**
     * Get plaintext from encrypted database value.
     *
     * @param string $value
     * @param string $key
     * @return string
     */
    public static function getPlaintext(string $value, string $key): string {
        // Check if it is an encryptedValue otherwise return value
        if (self::checkForEncryptedValue($value)) {
            // Get all parameters from encrypted data
            $allParameter = self::getDecryptParameter($value);
            $cipherMethod = $allParameter[ENCRYPTION_CIPHER_METHOD];
            $iv = $allParameter[ENCRYPTION_IV];
            $ciphertext = $allParameter[ENCRYPTION_CIPHERTEXT];
            return self::decrypt($ciphertext, $key, $cipherMethod, $iv);
        } else {
            return $value;
        }
    }

    /**
     * Get needed openssl decrypt parameter from column value.
     *
     * Returns one parameter or all in array.
     * Expected value -> $encryptedData$:cipherMethod:iv:ciphertext
     * @param string $value
     * @param string $parameterName
     * @return string | array
     */
    public static function getDecryptParameter(string $value, string $parameterName = 'all') {
        $allParameter = array(
            ENCRYPTION_KEYWORD_ENCRYPTED_DATA => '',
            ENCRYPTION_IV => '',
            ENCRYPTION_CIPHER_METHOD => '',
            ENCRYPTION_CIPHERTEXT => ''
        );

        // Try to split value and check for first element of array
        if (self::checkForEncryptedValue($value)) {
            $array = explode(':', $value, 4);
            $encryptionKeyword = $array[0];
            $allParameter[ENCRYPTION_KEYWORD_ENCRYPTED_DATA] = $array[0];
            $cipherMethod = $array[1];
            $allParameter[ENCRYPTION_CIPHER_METHOD] = $array[1];
            $iv = $array[2];
            $allParameter[ENCRYPTION_IV] = $array[2];
            $ciphertext = $array[3];
            $allParameter[ENCRYPTION_CIPHERTEXT] = $array[3];
        } else {
            return false;
        }

        // Return asked parameter
        switch ($parameterName) {
            case ENCRYPTION_KEYWORD_ENCRYPTED_DATA:
                return $encryptionKeyword;
            case ENCRYPTION_IV:
                return $iv;
            case ENCRYPTION_CIPHER_METHOD:
                return $cipherMethod;
            case ENCRYPTION_CIPHERTEXT:
                return $ciphertext;
            default:
                return $allParameter;
        }
    }

    /**
     * Return ciphertext from value.
     *
     * @param string $value
     * @return string
     */
    public static function getCiphertext(string $value): string {
        return self::getDecryptParameter($value, ENCRYPTION_CIPHERTEXT);
    }

    /**
     * Check for valid encryption method.
     *
     *
     * @param string $value
     * @return boolean
     */
    public static function checkForValidEncryptMethod(string $value): bool {
        $validEncryptionMethods = array(
            ENCRYPTION_METHOD_AES_128,
            ENCRYPTION_METHOD_AES_256
        );

        foreach ($validEncryptionMethods as $method) {
            if ($method === $value) {
                return true;
            }
        }
        return false;
    }

    /**
     * Check for valid database field type.
     *
     *
     * @param string $dbIndex
     * @param string $dbFieldType
     *
     * @return boolean
     * @throws CodeException
     */
    public static function validDatabaseFieldType(string $dbIndex, string &$dbFieldType): bool {
        $validDbTypeField = false;
        try {
            $store = Store::getInstance();
            $dbIndexData = $dbIndex;
            $dbArray[$dbIndexData] = new Database($dbIndexData);
            $indexQfq = $store->getVar(SYSTEM_DB_INDEX_QFQ, STORE_SYSTEM, SANITIZE_ALLOW_ALL);
            $referenceFieldName = $store->getVar(F_NAME, STORE_FORM, SANITIZE_ALLOW_ALL);
            // Get form id from needed form -> form in which inside is actual form element
            $referenceFormId = $store->getVar(FE_FORM_ID, STORE_FORM, SANITIZE_ALLOW_ALL);

            // Get the right table name with the form id
            if ($referenceFormId) {
                if ($indexQfq != $dbIndexData) {
                    $dbArrayQfq[$indexQfq] = new Database($indexQfq);
                    $tableNameArray = $dbArrayQfq[$indexQfq]->getTableNameById($referenceFormId);
                } else {
                    $tableNameArray = $dbArray[$dbIndexData]->getTableNameById($referenceFormId);
                }
                $referenceTableName = $tableNameArray[0][F_TABLE_NAME];
                $tableDefinition = $dbArray[$dbIndexData]->getTableDefinition($referenceTableName);
                // Get dbFieldType from table definition
                foreach ($tableDefinition as $column) {
                    if ($column['Field'] === $referenceFieldName) {
                        $dbFieldType = $column['Type'];
                        break;
                    }
                }

                $dbFieldType = explode('(', $dbFieldType, 2);
                $dbFieldType = $dbFieldType[0];
                if ($dbFieldType === 'text' || $dbFieldType === 'varchar') {
                    $validDbTypeField = true;
                }
            }

        } catch (CodeException|\UserReportException|\UserFormException|\DbException $e) {
            // Show error if method not finished right
            throw new CodeException("Unexpected Problem in encryption class.", ERROR_ENCRYPT_CLASS);
        }

        return $validDbTypeField;
    }

    /**
     * Return array with form element infos which needed to be encrypted.
     *
     *
     * @param array $formSpecNative
     *
     * @return array
     * @throws CodeException
     */
    public static function getElementsToEncrypt(array $formSpecNative): array {
        try {
            $store = Store::getInstance();
            $formNewValues = $store->getStore(STORE_FORM);
            $defaultEncryptionMethod = $store->getVar(SYSTEM_ENCRYPTION_METHOD, STORE_SYSTEM, SANITIZE_ALLOW_ALL);
            $encryptionKey = trim($store->getVar(SYSTEM_ENCRYPTION_KEY, STORE_SYSTEM), " ");
        } catch (CodeException|\UserReportException|\UserFormException|\DbException $e) {
            // Error if not able to get store instance
            throw new CodeException("Unexpected problem in encryption class.", ERROR_ENCRYPT_CLASS);
        }

        $formNewValues = array_splice($formNewValues, 1);
        $elementsToEncrypt = array();
        $countArray = 0;

        foreach ($formSpecNative as $formElement) {
            if (($formElement[FE_ENCRYPTION] ?? '') === 'yes' && empty($formElement[FE_RETYPE_SOURCE_NAME])) {
                $elementsToEncrypt[$countArray][FE_NAME] = $formElement[FE_NAME];
                $value = $formNewValues[$formElement[FE_NAME]];

                if ($formElement[ENCRYPTION_METHOD_KEYWORD] === 'Default') {
                    $elementsToEncrypt[$countArray][ENCRYPTION_METHOD_KEYWORD] = $defaultEncryptionMethod;
                } else {
                    $elementsToEncrypt[$countArray][ENCRYPTION_METHOD_KEYWORD] = $formElement[ENCRYPTION_METHOD_KEYWORD];
                }

                $encryptionMethod = $elementsToEncrypt[$countArray][ENCRYPTION_METHOD_KEYWORD];
                $elementsToEncrypt[$countArray][ENCRYPTION_VALUE] = EncryptDecrypt::buildDataBaseValue($value, $encryptionKey, $encryptionMethod);
                $countArray++;
            }
        }

        return $elementsToEncrypt;
    }

    /**
     * Return array with database field information -> name, type, length.
     *
     *
     * @param object $dbObject
     * @param string $referenceTableName
     *
     * @return array
     */
    public static function getDbFieldAttributes(string $referenceTableName, object $dbObject): array {
        $dbFieldAttributes = array();

//        $tableNameArray = $dbObject->getTableNameById($formId);
//        $referenceTableName = $tableNameArray[0][F_TABLE_NAME];
        $tableDefinition = $dbObject->getTableDefinition($referenceTableName);

        foreach ($tableDefinition as $column => $attribute) {
            $dummyField = explode('(', $attribute['Type'], 2);

            if (isset($dummyField[1]) && $dummyField[1] !== '') {
                $dataFieldType = $dummyField[0];
                $dataFieldLength = explode(')', $dummyField[1])[0];
            } else {
                $dataFieldType = $attribute['Type'];
                $dataFieldLength = '';
            }

            $dbFieldAttributes[$attribute['Field']][DB_COLUMN_NAME] = $attribute['Field'];
            $dbFieldAttributes[$attribute['Field']][DB_COLUMN_TYPE] = $dataFieldType;
            $dbFieldAttributes[$attribute['Field']][DB_COLUMN_LENGTH] = $dataFieldLength;
        }

        return $dbFieldAttributes;
    }

    /**
     * Check if there is enough space for encrypted values in database
     *
     *
     * @param array $dbFieldAttributes
     * @param array $elementsToEncrypt
     *
     * @return bool
     * @throws \UserFormException
     */
    public static function hasEnoughSpace(array $elementsToEncrypt, array $dbFieldAttributes): bool {
        foreach ($elementsToEncrypt as $element => $attribute) {
            if ($dbFieldAttributes[$attribute[FE_NAME]][DB_COLUMN_TYPE] === 'varchar') {
                $countChars = strlen($attribute[ENCRYPTION_VALUE]);
                if ($countChars > $dbFieldAttributes[$attribute[FE_NAME]][DB_COLUMN_LENGTH]) {
                    throw new \UserFormException("Unexpected Problem. Please contact admin. Code: " . ERROR_DATABASE_FIELD_TO_SMALL, ERROR_DATABASE_FIELD_TO_SMALL);
                }
            }
        }

        return true;
    }

    /**
     * Checks for encrypted database value.
     *
     * Looking for string $encryptedData$.
     *
     * @param mixed $value
     * @return boolean
     */
    public static function checkForEncryptedValue($value): bool {
        if (is_null($value)) {
            return false;
        }

        // Split array in value.
        if (is_array($value)) {
            $value = OnArray::getFirstValueFromArray($value);
        }

        // Try to split value and check for first element of array
        // Value example: $encryptedData$:AES-128:55623454572345:df245f3246g4356ghsadfnghj656
        $arrayValues = explode(':', $value, 4);
        if (isset($arrayValues[0]) && $arrayValues[0] !== '') {
            if ($arrayValues[0] === ENCRYPTION_KEYWORD_ENCRYPTED_DATA) {
                return true;
            }
        }

        return false;
    }
}