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

use Fifa\ConnectServiceBus\Sdk\Api\ApiException;
use Fifa\ConnectServiceBus\Sdk\Encryption\Decrypt\PrivateCertificateStorageInterface;
use Fifa\ConnectServiceBus\Sdk\Encryption\Encrypt\PublicCertificateProviderInterface;
use Fifa\ConnectServiceBus\Sdk\Encryption\Model\PrivateCertificate;
use Fifa\ConnectServiceBus\Sdk\Exception\CryptographyException;
use Fifa\ConnectServiceBus\Sdk\Exception\DecryptionException;
use Fifa\ConnectServiceBus\Sdk\Message\Message;
use Psr\Log\LoggerInterface;

/**
 * Class RSACryptographyService
 * @package Fifa\ConnectServiceBus\Sdk\Encryption
 */
class RSACryptographyService implements CertificateBasedCryptographyServiceInterface
{
    /**
     * @var PublicCertificateProviderInterface
     */
    private $publicCertificateProvider;

    /**
     * @var PrivateCertificateStorageInterface
     */
    private $privateCertificateStorage;

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

    /**
     * @param PublicCertificateProviderInterface $publicCertificateProvider
     * @param PrivateCertificateStorageInterface $privateCertificateStorage
     * @param LoggerInterface $logger
     */
    public function __construct(PublicCertificateProviderInterface $publicCertificateProvider, PrivateCertificateStorageInterface $privateCertificateStorage,
                                LoggerInterface $logger)
    {
        $this->publicCertificateProvider = $publicCertificateProvider;
        $this->privateCertificateStorage = $privateCertificateStorage;
        $this->logger = $logger;
    }

    /**
     * Encrypts given data
     *
     * @param string $data
     * @param string $queueIdentifier
     * @return string
     * @throws CryptographyException
     */
    public function encrypt(string $data, string $queueIdentifier): string
    {
        $certificate = $this->publicCertificateProvider->get($queueIdentifier);

        $encryptedData = '';
        if (!openssl_public_encrypt($data, $encryptedData, $certificate->getKey())){
            throw new CryptographyException(new ApiException('Data encryption failed'));
        }

        return $encryptedData;
    }

    /**
     * Decrypts given string
     *
     * @param string $data
     * @param Message $message - Service Bus message with encrypted content
     * @return string
     * @throws CryptographyException
     */
    public function decrypt(string $data, Message $message): string
    {
        $decryptedData = '';

        if ($this->tryDecrypt($this->privateCertificateStorage->getLatestKey(), $data, 'latest', $decryptedData)) {
            return $decryptedData;
        }

        foreach ($this->privateCertificateStorage->getPreviousKeys() as $certificate) {
            if ($this->tryDecrypt($certificate, $data, 'previous', $decryptedData)) {
                return $decryptedData;
            }
        }

        throw new DecryptionException($message, new ApiException('Could not decrypt the data with any of the private certificates.'));
    }

    /**
     * Try to decrypt given data
     *
     * @param PrivateCertificate $privateKey
     * @param string $data
     * @param string $keyName
     * @param string &$result
     * @return bool
     */
    private function tryDecrypt(PrivateCertificate $privateKey, string $data, string $keyName, string &$result): bool
    {
        $decrypted = openssl_private_decrypt($data, $result, openssl_pkey_get_private($privateKey->getKey(), $privateKey->getPassword()));
        if (!$decrypted) {
            $this->logger->error('Data wasn\'t decrypted correctly with ' . $keyName . ' key.', array('data' => $data));
        }

        return $decrypted;
    }
}