<?php
namespace Fifa\ConnectServiceBus\Sdk\Encryption;

use Fifa\ConnectServiceBus\Sdk\Api\ApiException;
use Fifa\ConnectServiceBus\Sdk\Encryption\Model\EncryptionResult;
use Fifa\ConnectServiceBus\Sdk\Exception\CryptographyException;
use Fifa\ConnectServiceBus\Sdk\Exception\DecryptionException;
use Fifa\ConnectServiceBus\Sdk\Message\Message;
use InvalidArgumentException;
use phpseclib\Crypt\AES;
use Psr\Log\LoggerInterface;

/**
 * Class CertificateBasedCryptographyAlgorithm
 * @package Fifa\ConnectServiceBus\Sdk\Encryption
 */
class CertificateBasedCryptographyAlgorithm implements CryptographyAlgorithmInterface
{
    /**
     * @var int
     */
    private $keySize = 128;

    /**
     * @var string
     */
    private $version = '1';

    /**
     * @var CertificateBasedCryptographyServiceInterface
     */
    private $cryptographyService;

    /**
     * @var LoggerInterface
     */
    private $logger;

    /**
     * Constructor
     *
     * @param CertificateBasedCryptographyServiceInterface $cryptographyService
     * @param LoggerInterface $logger
     */
    public function __construct(CertificateBasedCryptographyServiceInterface $cryptographyService, LoggerInterface $logger)
    {
        $this->cryptographyService = $cryptographyService;
        $this->logger = $logger;
    }

    /**
     * Returns string representation of the object
     * @return string
     */
    public function __toString()
    {
        return 'KeySize: ' . $this->keySize . ', Version: ' . $this->version;
    }

    /**
     * Encrypts data for specific queueIdentifier
     *
     * @param string|null $data - Data for encryption
     * @param string $queueIdentifier
     * @return EncryptionResult
     * @throws CryptographyException
     */
    public function encrypt(?string $data, string $queueIdentifier): EncryptionResult
    {
        $this->logger->debug('Started encryption of message content for organisation "' . $queueIdentifier .'" using symmetric algorithm');

        $key = openssl_random_pseudo_bytes($this->keySize / 8);
        $this->logger->debug('Encrypting generated key using asymmetric algorithm for organisation ' . $queueIdentifier);
        $encryptedKey = $this->cryptographyService->encrypt($key, $queueIdentifier);
        $this->logger->debug('Key encrypted successfully organisation ' .$queueIdentifier);

        $cipher = new AES();
        $initializationVector = openssl_random_pseudo_bytes($cipher->getBlockLength() >> 3);
        $cipher->setKey($key);
        $cipher->setIV($initializationVector);

        $this->logger->debug('Encrypting provided content using generated key and vector for organisation ' . $queueIdentifier);
        $encryptedData = $cipher->encrypt($data);

        if (!is_string($encryptedData) || strlen($encryptedData) == 0) {
            throw new CryptographyException(new ApiException('Unable to encrypt content for organisation ' . $queueIdentifier));
        }
        $this->logger->debug('Message content encrypted successfully for organisation ' . $queueIdentifier);

        $encryptionResult = new EncryptionResult();
        $encryptionResult->setData($encryptedData);
        $encryptionResult->setProperties(array(
            'EncryptionKey' => base64_encode($encryptedKey),
            'EncryptionInitializationVector' => base64_encode($initializationVector)
        ));

        return $encryptionResult;
    }

    /**
     * Decrypts data
     *
     * @param Message $message - Service Bus message with encrypted content
     * @return string
     * @throws CryptographyException
     */
    public function decrypt(Message $message): string
    {
        $properties = $message->getProperties();
        if (!isset($properties['EncryptionKey']) || !is_string($properties['EncryptionKey']) || strlen($properties['EncryptionKey']) === 0) {
            throw new DecryptionException($message, new ApiException('Required "EncryptionKey" parameter was not found in $properties array.'));
        }

        if (!isset($properties['EncryptionInitializationVector']) || !is_string($properties['EncryptionInitializationVector'])
            || strlen($properties['EncryptionInitializationVector']) === 0) {
            throw new DecryptionException($message, new ApiException('Required "EncryptionInitializationVector" parameter was not found in $properties array.'));
        }

        $base64key = $properties['EncryptionKey'];
        $base64InitializationVector = $properties['EncryptionInitializationVector'];

        $this->logger->debug(
            'Starting decryption of message with provided parameters key: "' . $base64key . '", vector: "' . $base64InitializationVector . '"'
        );

        $encryptedKey = base64_decode($base64key);
        $initializationVector = base64_decode($base64InitializationVector);

        $this->logger->debug('Decrypting provided key.');
        $key = $this->cryptographyService->decrypt($encryptedKey, $message);
        $this->logger->debug('Key decrypted successfully.');

        $cipher = new AES();
        $cipher->setKey($key);
        $cipher->setIV($initializationVector);

        $this->logger->debug('Decrypting message content using symmetric algorithm.');
        $decryptedData = $cipher->decrypt($message->getContent());

        if (!is_string($decryptedData)) {
            throw new DecryptionException($message, new ApiException('Unable to decrypt content for provided key: "' . $base64key .
                '" and vector: "' . $base64InitializationVector . '"'));
        }

        return $decryptedData;
    }

    /**
     * Algorithm version
     *
     * @return string
     */
    public function version(): string
    {
        return $this->version;
    }

    /**
     * Gets key size
     *
     * @return int
     */
    public function getKeySize(): int
    {
        return $this->keySize;
    }
}