<?php

namespace Fifa\ConnectId\Sdk\Registration\Event;

use Exception;
use Fifa\ConnectId\Sdk\Exception\FifaConnectIdException;
use Fifa\ConnectId\Sdk\MessageHandlers\MessageHandlerInterface;
use Fifa\ConnectId\Sdk\Registration\PrefixedLogger;
use Fifa\ConnectId\Sdk\Util\CorrelationIdFormatter;
use Fifa\ConnectId\Sdk\Util\Data\Message\MessageProperties;
use Fifa\ConnectServiceBus\Sdk\Exception\CryptographyException;
use Fifa\ConnectServiceBus\Sdk\Exception\DecryptionException;
use Fifa\ConnectServiceBus\Sdk\FifaConnectServiceBusClient;
use Fifa\ConnectServiceBus\Sdk\FifaConnectServiceBusClientInterface;
use Fifa\ConnectServiceBus\Sdk\Message\Message;
use Psr\Log\LoggerInterface;

class ExtensibleMessageEventHandler
{
    /**
     * @var LoggerInterface
     */
    private $logger;

    /**
     * @var FifaConnectServiceBusClient
     */
    private $serviceBusClient;

    /**
     * @var int
     */
    private $receivingOperationExecutionTime;

    /**
     * @var int
     */
    private $exceptionCounterLimit = 10;

    /**
     * @var int
     */
    private $exceptionSleepTimeSeconds = 10;

    /**
     * Indicates a maximum number of messages that can be received in listener run.
     * @var int
     */
    private $stopAfter = null;

    /**
     * @var EventHandlerCollection
     */
    public $serviceBusExceptionEvent;

    /**
     * @var MessageHandlerInterface[]
     */
    private $messageHandlers;

    /**
     * @internal
     * ExtensibleMessageEventHandler constructor. Internal use only.
     */
    public function __construct(LoggerInterface $logger, FifaConnectServiceBusClientInterface $client, $receivingOperationExecutionTime, array $messageHandlers)
    {
        $this->logger = new PrefixedLogger($logger, '[LID: ' . uniqid() . '] ');
        $this->serviceBusClient = $client;
        $this->receivingOperationExecutionTime = $receivingOperationExecutionTime;
        $this->messageHandlers = $messageHandlers;
        $this->serviceBusExceptionEvent = new EventHandlerCollection();
    }

    /**
     * Run the listener (SDK part) that receives and handles the messages from service bus
     *
     * @return int
     * @throws FifaConnectIdException
     */
    public function run(): int
    {
        self::logMessageHandlers($this->messageHandlers, $this->logger);
        $startTime = time();
        $messageCounter = 0;
        $exceptionCounter = 0;

        while (true) {
            try {
                $message = $this->serviceBusClient->peekLock($this->receivingOperationExecutionTime);
                if (!is_null($message)) {
                    $this->logger->debug(self::getFormattedCorrelationId($message) . "Received and locked a message: " . $message);

                    $messageCanBeDeleted = true;
                    try {
                        $actionType = strtolower($message->getAction());

                        $messageHandler = $this->selectHandlerFor($actionType);
                        if($messageHandler == null) {
                            throw new Exception('Message with unrecognized action type \''.$actionType.'\' was not processed, because no generic message handler was provided.');
                        }

                        $this->logger->debug('Processing message using ' . $messageHandler->__toString());
                        $messageHandler->handle($actionType, $message);
                        $this->logger->debug('The message with id ' . $message->getId() . ' was processed properly. It will be deleted now.');
                    } catch (Exception $exception) {
                        $this->logger->error('The message with id ' . $message->getId() . ' was NOT processed properly. It will be unlocked now.',
                            array('exception' => $exception->__toString()));
                        $this->serviceBusClient->unlock($message->getId(), $message->getLockToken());
                        $messageCanBeDeleted = false;
                    }

                    if ($messageCanBeDeleted) {
                        $this->serviceBusClient->delete($message->getId(), $message->getLockToken());
                        $exceptionCounter = 0;
                    }
                }
            } catch (DecryptionException $e) {
                $this->logger->error(self::getFormattedCorrelationId($e->getServiceBusMessage()) . "Failed to decrypt content of received and locked message: " . $e->getServiceBusMessage());
                $this->letClientHandleServiceBusException($e, $e->getMessage());
            } catch (CryptographyException $e) {
                $this->letClientHandleServiceBusException($e, $e->getMessage());
            } catch (Exception $e) {
                $this->letClientHandleServiceBusException($e, 'Message processing resulted with an exception');

                if ($exceptionCounter > $this->exceptionCounterLimit) {
                    sleep($this->exceptionSleepTimeSeconds);
                    $exceptionCounter = 0;
                } else {
                    $exceptionCounter++;
                }
            }

            $messageCounter++;
            $timeDiff = time() - $startTime;

            if (($timeDiff > $this->receivingOperationExecutionTime) || (!is_null($this->stopAfter) && $messageCounter > $this->stopAfter)) {
                $this->logger->debug("Terminating...");
                break;
            }
        }

        return $messageCounter;
    }

    /**
     * Handler for FifaConnectServiceBusSdkException
     *
     * @param Exception $exception
     * @param string $context
     */
    function letClientHandleServiceBusException(Exception $exception, string $context)
    {
        try {
            $this->logger->debug('Letting the client to handle a Service Bus exception.');
            $this->logger->error($context, array('exception' => $exception->__toString()));
            $this->serviceBusExceptionEvent->raiseEvent($exception);
        } catch (Exception $e) {
            $this->logger->error('Exception was not properly handled by the client - an exception was thrown.', array('exception' => $e->__toString()));
        }
    }

    /**
     * Get a maximum number of messages that can be received in listener run.
     *
     * @return int|null
     */
    public function getStopAfter(): ?int
    {
        return $this->stopAfter;
    }

    /**
     * Set a maximum number of messages that can be received in listener run.
     *
     * @param int $stopAfter
     */
    public function setStopAfter(int $stopAfter)
    {
        $this->stopAfter = $stopAfter;
    }

    /**
     * @return int
     */
    public function getExceptionCounterLimit(): int
    {
        return $this->exceptionCounterLimit;
    }

    /**
     * @param int $exceptionCounterLimit
     */
    public function setExceptionCounterLimit(int $exceptionCounterLimit)
    {
        $this->exceptionCounterLimit = $exceptionCounterLimit;
    }

    /**
     * @return int
     */
    public function getExceptionSleepTimeSeconds(): int
    {
        return $this->exceptionSleepTimeSeconds;
    }

    /**
     * @param int $exceptionSleepTimeSeconds
     */
    public function setExceptionSleepTimeSeconds(int $exceptionSleepTimeSeconds)
    {
        $this->exceptionSleepTimeSeconds = $exceptionSleepTimeSeconds;
    }

    private function selectHandlerFor(string $actionType) {
        foreach ($this->messageHandlers as $handler) {
            if ($handler->canHandle($actionType)) {
                return $handler;
            }
        }
        return null;
    }

    private static function logMessageHandlers(array $messageHandlers, LoggerInterface $logger) {
        $message = 'Starting with the following handlers: ';
        foreach($messageHandlers as $messageHandler) {
            $message .= $messageHandler->__toString() . ', ';
        }
        $message = rtrim($message, ', ');
        $logger->debug($message);
    }

    private static function getFormattedCorrelationId(Message $message): string
    {
        return CorrelationIdFormatter::format($message->getPropertyIfExists(MessageProperties::CORRELATION_ID));
    }
}