<?php

namespace Fifa\ConnectId\Sdk\Tms;

use Exception;
use Fifa\ConnectId\Sdk\ApiClient\Tms\TmsApi;
use Fifa\ConnectId\Sdk\Exception\TmsOperationException;
use Fifa\ConnectId\Sdk\Model\DataStandard\PersonLocal;
use Fifa\ConnectId\Sdk\Model\Transfer\DomesticTransferDeclaration;
use Fifa\ConnectId\Sdk\Model\Transfer\DomesticTransferDeclarationResponse;
use Fifa\ConnectId\Sdk\Model\Transfer\FirstProRegistration;
use Fifa\ConnectId\Sdk\Model\Transfer\FirstProRegistrationResponse;
use Fifa\ConnectId\Sdk\Model\Transfer\ProofOfPayment;
use Fifa\ConnectId\Sdk\Model\Transfer\ProofOfPaymentResponse;
use Fifa\ConnectId\Sdk\Registration\CorrelationIdBasedItemProviderInterface;
use Fifa\ConnectId\Sdk\Registration\DeserializingCorrelationIdBasedItemProvider;
use Fifa\ConnectId\Sdk\Registration\ResponseAwaitersFactoryInterface;
use Fifa\ConnectId\Sdk\Util\CorrelationId;
use Fifa\ConnectId\Sdk\Util\CorrelationIdFormatter;
use Fifa\ConnectId\Sdk\Util\MessageSerializers\DomesticTransferDeclarationResponseXmlSerializer;
use Fifa\ConnectId\Sdk\Util\MessageSerializers\FirstProRegistrationResponseXmlSerializer;
use Fifa\ConnectId\Sdk\Util\MessageSerializers\ProofOfPaymentResponseXmlSerializer;
use Fifa\ConnectServiceBus\Sdk\FifaConnectServiceBusClientInterface;
use InvalidArgumentException;
use Psr\Log\LoggerInterface;

class TmsFacade implements TmsFacadeInterface
{
    /**
     * @var TmsApi
     */
    private $tmsApi;

    /**
     * @var ResponseAwaitersFactoryInterface
     */
    private $responseAwaitersFactory;

    /**
     * @var CorrelationIdBasedItemProviderInterface
     */
    private $domesticTransferConfirmationCache;

    /**
     * @var CorrelationIdBasedItemProviderInterface
     */
    private $firstProRegistrationConfirmationCache;

    /**
     * @var DeserializingCorrelationIdBasedItemProvider
     */
    private $proofOfPaymentConfirmationCache;

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

    private function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    /**
     * @param FifaConnectServiceBusClientInterface $serviceBusClient
     * @param LoggerInterface $logger
     * @param ResponseAwaitersFactoryInterface $responseAwaitersFactory
     * @param CorrelationIdBasedItemProviderInterface $messageReceivedCache
     * @return TmsFacade
     */
    public static function create(FifaConnectServiceBusClientInterface $serviceBusClient, LoggerInterface $logger,
                                  ResponseAwaitersFactoryInterface $responseAwaitersFactory, CorrelationIdBasedItemProviderInterface $messageReceivedCache): TmsFacade
    {
        $facade = new TmsFacade($logger);
        $facade->tmsApi = new TmsApi($serviceBusClient, $logger);
        $facade->responseAwaitersFactory = $responseAwaitersFactory;
        $facade->domesticTransferConfirmationCache = new DeserializingCorrelationIdBasedItemProvider($messageReceivedCache, new DomesticTransferDeclarationResponseXmlSerializer());
        $facade->firstProRegistrationConfirmationCache = new DeserializingCorrelationIdBasedItemProvider($messageReceivedCache, new FirstProRegistrationResponseXmlSerializer());
        $facade->proofOfPaymentConfirmationCache = new DeserializingCorrelationIdBasedItemProvider($messageReceivedCache, new ProofOfPaymentResponseXmlSerializer());

        return $facade;
    }

    /**
     * @inheritDoc
     */
    public function addPlayer(PersonLocal $personLocal): string
    {
        $correlationId = CorrelationId::create();
        try {
            $this->tmsApi->addPlayer($correlationId, $personLocal);
            return $correlationId;
        }
        catch (Exception $exception) {
            $this->logger->error(CorrelationIdFormatter::format($correlationId).'Failed to add player to TMS.', array('exception' => $exception->__toString()));
            throw $exception;
        }
    }

    /**
     * @inheritDoc
     */
    public function updatePlayer(PersonLocal $personLocal): string
    {
        $correlationId = CorrelationId::create();
        try {
            $this->tmsApi->updatePlayer($correlationId, $personLocal);
            return $correlationId;
        }
        catch (Exception $exception) {
            $this->logger->error(CorrelationIdFormatter::format($correlationId).'Failed to update player in TMS.', array('exception' => $exception->__toString()));
            throw $exception;
        }
    }

    /**
     * @inheritDoc
     * @throws ResponseNotReceivedException
     * @throws InvalidArgumentException
     * @throws TmsOperationException
     * @throws DeclarationRejectedException
     */
    public function declareDomesticTransferAndWaitForTransferId(DomesticTransferDeclaration $declaration, int $timeoutSeconds): int
    {
        $correlationId = CorrelationId::create();
        try {
            $responseAwaiter = $this->responseAwaitersFactory->create($this->domesticTransferConfirmationCache);
            $this->tmsApi->declareDomesticTransfer($correlationId, $declaration);
            $response = $responseAwaiter->waitForResponse($correlationId, $timeoutSeconds);
            if ($response == null) {
                throw new ResponseNotReceivedException('Transfer ID has not arrived within the specified timeout', $correlationId);
            }
            if (!($response instanceof DomesticTransferDeclarationResponse)) {
                throw new InvalidArgumentException(sprintf('Expected response to be of type "%s", got "%s"', DomesticTransferDeclarationResponse::class, get_class($response)));
            }
            if (!is_null($response->Error)) {
                throw new DeclarationRejectedException("Domestic Transfer Declaration", $response->Error->Message);
            }
            return $response->Confirmation->TmsTransferId;
        }
        catch (Exception $exception) {
            $this->logger->error(CorrelationIdFormatter::format($correlationId).'Failed to send Domestic Transfer Declaration', array('exception' => $exception->__toString()));
            throw $exception;
        }
    }

    /**
     * @inheritDoc
     * @throws ResponseNotReceivedException
     * @throws InvalidArgumentException
     * @throws TmsOperationException
     * @throws DeclarationRejectedException
     */
    public function provePaymentAndWaitForPaymentId(ProofOfPayment $proofOfPayment, int $timeoutSeconds): int
    {
        $correlationId = CorrelationId::create();
        try {
            $responseAwaiter = $this->responseAwaitersFactory->create($this->proofOfPaymentConfirmationCache);
            $this->tmsApi->provePayment($correlationId, $proofOfPayment);
            $response = $responseAwaiter->waitForResponse($correlationId, $timeoutSeconds);
            if ($response == null) {
                throw new ResponseNotReceivedException('Payment ID has not arrived within the specified timeout', $correlationId);
            }
            if (!($response instanceof ProofOfPaymentResponse)) {
                throw new InvalidArgumentException(sprintf('Expected response to be of type "%s", got "%s"', ProofOfPaymentResponse::class, get_class($response)));
            }
            if (!is_null($response->Error)) {
                throw new DeclarationRejectedException("Proof Of Payment", $response->Error->Message);
            }
            return $response->Confirmation->PaymentId;
        }
        catch (Exception $exception) {
            $this->logger->error(CorrelationIdFormatter::format($correlationId).'Failed to send Proof Of Payment', array('exception' => $exception->__toString()));
            throw $exception;
        }
    }

    /**
     * @inheritDoc
     * @throws ResponseNotReceivedException
     * @throws InvalidArgumentException
     * @throws TmsOperationException
     * @throws DeclarationRejectedException
     */
    public function declareFirstProRegistrationAndWaitForRegistrationId(FirstProRegistration $firstProRegistration, int $timeoutSeconds): int
    {
        $correlationId = CorrelationId::create();
        try {
            $responseAwaiter = $this->responseAwaitersFactory->create($this->firstProRegistrationConfirmationCache);
            $this->tmsApi->declareFirstProRegistration($correlationId, $firstProRegistration);
            $response = $responseAwaiter->waitForResponse($correlationId, $timeoutSeconds);
            if ($response == null) {
                throw new ResponseNotReceivedException('First Pro Registration ID has not arrived within the specified timeout', $correlationId);
            }
            if (!($response instanceof FirstProRegistrationResponse)) {
                throw new InvalidArgumentException(sprintf('Expected response to be of type "%s", got "%s"', FirstProRegistrationResponse::class, get_class($response)));
            }
            if (!is_null($response->Error)) {
                throw new DeclarationRejectedException("First Pro Registration", $response->Error->Message);
            }
            return $response->Confirmation->TmsFirstRegistrationId;
        } catch (Exception $exception) {
            $this->logger->error(CorrelationIdFormatter::format($correlationId).'Failed to send First Pro Registration', array('exception' => $exception->__toString()));
            throw $exception;
        }
    }
}
