<?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;

/**
 * Class MessageCryptographyService
 * @package Fifa\ConnectServiceBus\Sdk\Encryption
 */
class MessageCryptographyService implements MessageCryptographyServiceInterface
{
    const ENCRYPTION_VERSION_KEY = 'EncryptionVersion';

    /**
     * @var CryptographyAlgorithmInterface[]
     */
    private $algorithms;

    /**
     * Constructor
     *
     * @param CryptographyAlgorithmInterface[] $algorithms
     */
    public function __construct(array $algorithms)
    {
        if (sizeof($algorithms) === 0) {
            throw new InvalidArgumentException('Array with algorithms cannot be empty.', 1000);
        }

        $versions = array();
        foreach ($algorithms as $algorithm) {
            if (isset($versions[$algorithm->version()])) {
                throw new InvalidArgumentException('Collection of $algorithms cannot contain elements with the same version', 1001);
            }
            $versions[$algorithm->version()] = true;
        }

        usort($algorithms, function ($a, $b) {
            if ($a->version() > $b->version()) {
                return -1;
            } elseif ($a->version() < $b->version()) {
                return 1;
            } else return 0;
        });

        $this->algorithms = $algorithms;
    }

    /**
     * Decrypts given string
     *
     * @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[self::ENCRYPTION_VERSION_KEY])) {
            throw new DecryptionException($message, new ApiException('There is no "EncryptionVersion" parameter provided in $properties array.)', 1001));
        }

        $version = $properties[self::ENCRYPTION_VERSION_KEY];
        if (!self::isVersionNumberValid($version)) {
            throw new DecryptionException($message, new ApiException('Unable to parse algorithm version from ' . $version, 1002));
        }

        foreach ($this->algorithms as $algorithm) {
            if ($algorithm->version() == $version) {
                $selectedAlgorithm = $algorithm;
                break;
            }
        }

        if (!isset($selectedAlgorithm)) {
            throw new DecryptionException($message, new ApiException(
                'There is no cryptography algorithm that can support "' . $version . '" version.'
            ));
        }

        return $selectedAlgorithm->decrypt($message);
    }

    /**
     * Encrypts given data
     *
     * @param string|null $data - Content that need to be encrypted
     * @param string $queueIdentifier
     * @return EncryptionResult
     * @throws CryptographyException
     */
    public function encrypt(?string $data, string $queueIdentifier): EncryptionResult
    {
        $selectedAlgorithm = $this->algorithms[0];
        $version = $selectedAlgorithm->version();
        if (!self::isVersionNumberValid($version)) {
            throw new CryptographyException(new ApiException(
                'Algorithm version needs to be a string.'
            ));
        }

        $result = $selectedAlgorithm->encrypt($data, $queueIdentifier);

        $properties = $result->getProperties();
        $properties[self::ENCRYPTION_VERSION_KEY] = $version;
        $result->setProperties($properties);

        return $result;
    }

    /**
     * Validates version number
     * @param string $version
     * @return bool
     */
    private static function isVersionNumberValid(string $version): bool
    {
        return (strlen($version) !== 0 && version_compare($version, '0.0.1', '>='));
    }

}