<?php

namespace Mautic\CoreBundle\Helper;

use Mautic\CoreBundle\Security\Cryptography\Cipher\Symmetric\SymmetricCipherInterface;
use Mautic\CoreBundle\Security\Exception\Cryptography\Symmetric\InvalidDecryptionException;

class EncryptionHelper
{
    /**
     * @var SymmetricCipherInterface[]
     */
    private ?array $availableCiphers = null;

    /**
     * @var string
     */
    private $key;

    public function __construct(
        CoreParametersHelper $coreParametersHelper,
    ) {
        $nonCipherArgs = 1;
        for ($i = $nonCipherArgs; $i < func_num_args(); ++$i) {
            $possibleCipher = func_get_arg($i);
            if (!($possibleCipher instanceof SymmetricCipherInterface)) {
                throw new \InvalidArgumentException($possibleCipher::class.' has to implement '.SymmetricCipherInterface::class);
            }
            if (!$possibleCipher->isSupported()) {
                continue;
            }
            $this->availableCiphers[] = $possibleCipher;
        }

        if (!$this->availableCiphers || 0 === count($this->availableCiphers)) {
            throw new \RuntimeException('None of possible cryptography libraries is supported');
        }

        $this->key = $coreParametersHelper->get('mautic.secret_key');
    }

    /**
     * Returns a 64 character string.
     */
    public static function generateKey(): string
    {
        return hash('sha256', uniqid(mt_rand(), true));
    }

    /**
     * Encrypt string.
     *
     * @param mixed $data
     */
    public function encrypt($data): string
    {
        $encryptionCipher = reset($this->availableCiphers);
        $initVector       = $encryptionCipher->getRandomInitVector();
        $encrypted        = $encryptionCipher->encrypt(serialize($data), $this->key, $initVector);

        return base64_encode($encrypted).'|'.base64_encode($initVector);
    }

    /**
     * Decrypt string.
     * Returns false in case of failed decryption.
     *
     * @param string $data
     * @param bool   $mainDecryptOnly
     *
     * @return mixed|false
     */
    public function decrypt($data, $mainDecryptOnly = false)
    {
        $encryptData      = explode('|', $data);
        $encryptedMessage = base64_decode($encryptData[0]);
        $initVector       = base64_decode($encryptData[1]);
        $mainTried        = false;
        foreach ($this->availableCiphers as $availableCipher) {
            if ($mainDecryptOnly && $mainTried) {
                return false;
            }
            try {
                return Serializer::decode($availableCipher->decrypt($encryptedMessage, $this->key, $initVector));
            } catch (InvalidDecryptionException) {
            }
            $mainTried = true;
        }

        return false;
    }
}
