<?php
namespace Fifa\ConnectServiceBus\Sdk;

use Exception;
use Fifa\ConnectServiceBus\Sdk\Api\ApiException;
use Fifa\ConnectServiceBus\Sdk\Authentication\Model\ClientCredentials;
use Fifa\ConnectServiceBus\Sdk\Encryption\CertificateBasedCryptographyAlgorithm;
use Fifa\ConnectServiceBus\Sdk\Encryption\Decrypt\MessageDecryptionServiceInterface;
use Fifa\ConnectServiceBus\Sdk\Encryption\Decrypt\PrivateCertificateStorageInterface;
use Fifa\ConnectServiceBus\Sdk\Encryption\Encrypt\MessageEncryptionServiceInterface;
use Fifa\ConnectServiceBus\Sdk\Encryption\Encrypt\PublicCertificateProvider;
use Fifa\ConnectServiceBus\Sdk\Encryption\MessageCryptographyService;
use Fifa\ConnectServiceBus\Sdk\Encryption\RSACryptographyService;
use Fifa\ConnectServiceBus\Sdk\Environment\EnvironmentInterface;
use Fifa\ConnectServiceBus\Sdk\ExceptionHandler\ExceptionHandler;
use Fifa\ConnectServiceBus\Sdk\ExceptionHandler\Handler\InvalidClientDataHandler;
use Fifa\ConnectServiceBus\Sdk\ExceptionHandler\Handler\MessageNotFoundHandler;
use Fifa\ConnectServiceBus\Sdk\ExceptionHandler\Handler\QueueNotFoundHandler;
use Fifa\ConnectServiceBus\Sdk\Message\Message;
use Fifa\ConnectServiceBus\Sdk\Api\Resource\MessageApi;
use Fifa\ConnectServiceBus\Sdk\Utils\ToStringConverter;
use Psr\Log\LoggerInterface;

/**
 * Class FifaConnectServiceBusClient
 * @package Fifa\ConnectServiceBus\Sdk
 */
class FifaConnectServiceBusClient implements FifaConnectServiceBusClientInterface
{
    /**
     * @var MessageApi
     */
    private $messageApi;

    /**
     * @var MessageEncryptionServiceInterface
     */
    private $encryptionService;

    /**
     * @var MessageDecryptionServiceInterface
     */
    private $decryptionService;

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

    /**
     * Constructor
     *
     * @param MessageApi $messageApi
     * @param MessageEncryptionServiceInterface $encryptionService
     * @param MessageDecryptionServiceInterface $decryptionService
     * @param LoggerInterface $logger
     */
    public function __construct(MessageApi $messageApi, MessageEncryptionServiceInterface $encryptionService,
                                MessageDecryptionServiceInterface $decryptionService, LoggerInterface $logger)
    {
        $this->messageApi = $messageApi;
        $this->decryptionService = $decryptionService;
        $this->logger = $logger;
        $this->encryptionService = $encryptionService;
    }

    /**
     * Creates an instance of FifaConnectServiceBusClient
     *
     * @param EnvironmentInterface $environment
     * @param ClientCredentials $clientCredentials
     * @param PrivateCertificateStorageInterface $privateCertificateStorage
     * @param LoggerInterface $logger
     * @return FifaConnectServiceBusClientInterface
     */
    public static function create(EnvironmentInterface $environment, ClientCredentials $clientCredentials,
                                  PrivateCertificateStorageInterface $privateCertificateStorage, LoggerInterface $logger): FifaConnectServiceBusClientInterface
    {
        $algorithms = array(
            new CertificateBasedCryptographyAlgorithm(new RSACryptographyService(
                new PublicCertificateProvider(
                    new FifaConnectServiceBusCertificateClient($environment, $clientCredentials, $logger),
                    $environment->getCacheStorage()
                ),
                $privateCertificateStorage,
                $logger
            ), $logger)
        );

        $messageService = new MessageCryptographyService($algorithms);
        $messageApi = $environment->getMessageApi($clientCredentials);

        return new FifaConnectServiceBusClient($messageApi, $messageService, $messageService, $logger);
    }

    /**
     * @inheritDoc
     */
    public function send(string $recipient, Message $message)
    {
        $this->logger->debug(sprintf('Sending message to %s', $recipient), array(
            'message' => $message->__toString()));
        try {

            $content = $message->getContent();
            $properties = $message->getProperties();

            $this->logger->debug('Encrypting message to be sent to: ' . $recipient);
            $encryptionResult = $this->encryptionService->encrypt($content, $recipient);

            $content = $encryptionResult->getData();
            $properties = array_merge($properties, $encryptionResult->getProperties());

            list($response, $statusCode, $httpHeaders) = $this->messageApi->messageSendByRecipientAndActionAndPropertiesWithHttpInfo(
                $recipient,
                json_encode(base64_encode($content)),
                $message->getAction(),
                json_encode($properties)
            );

            if ($statusCode != self::HTTP_CODE_CREATED) {
                throw new ApiException('Unexpected result', $statusCode, $httpHeaders, $response);
            }

            $this->logResponse($response, $statusCode, $httpHeaders);
        }
        catch (Exception $e) {
            $this->logger->error('Sending message error.', array('exception' => $e->__toString()));
            throw ExceptionHandler::create($e, new QueueNotFoundHandler());
        }
    }

    /**
     * @inheritDoc
     */
    public function peekLock(int $timeout = 60): ?Message
    {
        $this->logger->debug('Creating peek lock', array('timeout' => strval($timeout)));
        try {
            list($response, $statusCode, $httpHeaders) = $this->messageApi->messagePeekLockByRequestTimeoutWithHttpInfo($timeout);

            $this->logResponse($response, $statusCode, $httpHeaders);
            $message = $this->createMessage($response, $statusCode, $httpHeaders, self::HTTP_CODE_CREATED);

            if ($message == null) {
                return null;
            }

            $this->logger->debug('Message received from "' . $message->getSender() . '" appears to be encrypted. It will be now decrypted.');
            $message->setContent($this->decryptionService->decrypt($message));

            return $message;
        }
        catch (Exception $e) {
            $this->logger->error('Creating peek lock error.', array('exception' => $e->__toString()));
            throw ExceptionHandler::create($e, new QueueNotFoundHandler());
        }
    }

    /**
     * @inheritDoc
     */
    public function delete(string $messageId, string $lockToken)
    {
        $this->logger->debug(sprintf('Deleting message id "%s" (lock token "%s")', $messageId, $lockToken));
        try {
            list($response, $statusCode, $httpHeaders) = $this->messageApi->messageDeleteByRequestMessageidAndRequestLocktokenWithHttpInfo($messageId, $lockToken);

            if ($statusCode != self::HTTP_CODE_OK) {
                throw new ApiException('Unexpected result', $statusCode, $httpHeaders, $response);
            }

            $this->logResponse($response, $statusCode, $httpHeaders);
        }
        catch (Exception $e) {
            $this->logger->error('Deleting message error.', array('exception' => $e->__toString()));
            throw ExceptionHandler::create($e, new MessageNotFoundHandler(), new InvalidClientDataHandler());
        }
    }

    /**
     * @inheritDoc
     */
    public function unlock(string $messageId, string $lockToken)
    {
        $this->logger->debug(sprintf('Unlocking message id "%s" (lock token "%s")', $messageId, $lockToken));
        try {
            list($response, $statusCode, $httpHeaders) = $this->messageApi->messageUnlockByRequestMessageidAndRequestLocktokenWithHttpInfo($messageId, $lockToken);

            if ($statusCode != self::HTTP_CODE_OK) {
                throw new ApiException('Unexpected result.', $statusCode, $httpHeaders, $response);
            }
            $this->logResponse($response, $statusCode, $httpHeaders);
        }
        catch (Exception $e) {
            $this->logger->error('Unlocking message error.', array('exception' => $e->__toString()));
            throw ExceptionHandler::create($e, new MessageNotFoundHandler(), new InvalidClientDataHandler());
        }
    }

    /**
     * @param array $response
     * @param int $statusCode
     * @param array $httpHeaders
     * @param int $expectedSuccessStatusCode
     * @return Message
     *
     * @throws ApiException
     */
    private function createMessage(string $response, int $statusCode, array $httpHeaders, int $expectedSuccessStatusCode = self::HTTP_CODE_OK): ?Message
    {
        if ($statusCode == self::HTTP_CODE_NO_CONTENT) {
            $this->logger->debug('No messages available within the specified timeout period.');
            return null;
        }

        if ($statusCode == $expectedSuccessStatusCode && !empty($response)) {
            return new Message(
                base64_decode($response),
                $httpHeaders['X-Action'] ?? null,
                isset($httpHeaders['X-Properties']) ? json_decode($httpHeaders['X-Properties'], true) : array(),
                isset($httpHeaders['BrokerProperties']) ? json_decode($httpHeaders['BrokerProperties'], true) : array()
            );
        }

        throw new ApiException('Unexpected result', $statusCode, $httpHeaders, $response);
    }

    /**
     * @param mixed $response
     * @param int $statusCode
     * @param array $httpHeaders
     */
    private function logResponse($response, int $statusCode, array $httpHeaders)
    {
        $this->logger->debug('Received response from Connect Service Bus service.', array(
            'response'   => ToStringConverter::mixedToString($response),
            'statusCode' => strval($statusCode),
            'httpHeader' => ToStringConverter::arrayToString($httpHeaders)
        ));
    }
}