<?php

namespace FiloBlu\Esb\Framework\Event;

use DateTime;
use Exception;
use FiloBlu\Esb\Api\ConsumerConfigurationRepositoryInterface;
use FiloBlu\Esb\Api\Data\ConsumerConfigurationInterface;
use FiloBlu\Esb\Api\Data\EventConfigurationInterface;
use FiloBlu\Esb\Api\Data\EventInterface;
use FiloBlu\Esb\Api\Data\IdentifiableInterface;
use FiloBlu\Esb\Api\Data\MessageInterface;
use FiloBlu\Esb\Api\Data\MessageInterfaceFactory;
use FiloBlu\Esb\Api\Data\ProducerConfigurationInterface;
use FiloBlu\Esb\Api\Data\QueueItemInterface;
use FiloBlu\Esb\Api\Data\QueueItemInterfaceFactory;
use FiloBlu\Esb\Api\Data\QueueMetadataInterface;
use FiloBlu\Esb\Api\Data\QueueMetadataInterfaceFactory;
use FiloBlu\Esb\Api\Data\StatusInterface;
use FiloBlu\Esb\Api\Data\StatusInterfaceFactory;
use FiloBlu\Esb\Api\EventConfigurationRepositoryInterface;
use FiloBlu\Esb\Api\ProducerConfigurationRepositoryInterface;
use FiloBlu\Esb\Api\PublisherResolverInterface;
use FiloBlu\Esb\Api\QueueItemRepositoryInterface;
use Magento\Framework\Api\SearchCriteriaBuilder;
use Magento\Framework\Api\SearchCriteriaBuilderFactory;
use Magento\Framework\Exception\NoSuchEntityException;
use Psr\Log\LoggerInterface;
use Throwable;

/**
 * Class EventEmitter
 * @package FiloBlu\Esb\Framework\Event
 */
class EventEmitter
{
    /**
     * @var SearchCriteriaBuilderFactory
     */
    protected $searchCriteriaBuilderFactory;
    /**
     * @var PublisherResolverInterface
     */
    private $publisherResolver;

    /**
     * @var EventConfigurationRepositoryInterface
     */
    private $eventConfigurationRepository;
    /**
     * @var SearchCriteriaBuilder
     */
    private $searchCriteriaBuilder;
    /**
     * @var ProducerConfigurationRepositoryInterface
     */
    private $producerConfigurationRepository;
    /**
     * @var LoggerInterface
     */
    private $logger;
    /**
     * @var QueueMetadataInterfaceFactory
     */
    private $queueMetadataFactory;
    /**
     * @var ConsumerConfigurationRepositoryInterface
     */
    private $consumerConfigurationRepository;
    /**
     * @var QueueItemRepositoryInterface
     */
    private $queueItemRepository;
    /**
     * @var QueueItemInterfaceFactory
     */
    private $queueItemFactory;
    /**
     * @var MessageInterfaceFactory
     */
    private $messageFactory;
    /**
     * @var StatusInterfaceFactory
     */
    private $statusFactory;

    /**
     * OrderSaveAfter constructor.
     * @param PublisherResolverInterface $publisherResolver
     * @param SearchCriteriaBuilder $searchCriteriaBuilder
     * @param EventConfigurationRepositoryInterface $eventConfigurationRepository
     * @param ProducerConfigurationRepositoryInterface $producerConfigurationRepository
     * @param QueueMetadataInterfaceFactory $queueMetadataFactory
     * @param ConsumerConfigurationRepositoryInterface $consumerConfigurationRepository
     * @param SearchCriteriaBuilderFactory $searchCriteriaBuilderFactory
     * @param QueueItemRepositoryInterface $queueItemRepository
     * @param QueueItemInterfaceFactory $queueItemFactory
     * @param MessageInterfaceFactory $messageFactory
     * @param StatusInterfaceFactory $statusFactory
     * @param LoggerInterface $logger
     */
    public function __construct(
        PublisherResolverInterface $publisherResolver,
        SearchCriteriaBuilder $searchCriteriaBuilder,
        EventConfigurationRepositoryInterface $eventConfigurationRepository,
        ProducerConfigurationRepositoryInterface $producerConfigurationRepository,
        QueueMetadataInterfaceFactory $queueMetadataFactory,
        ConsumerConfigurationRepositoryInterface $consumerConfigurationRepository,
        SearchCriteriaBuilderFactory $searchCriteriaBuilderFactory,
        QueueItemRepositoryInterface $queueItemRepository,
        QueueItemInterfaceFactory $queueItemFactory,
        MessageInterfaceFactory $messageFactory,
        StatusInterfaceFactory $statusFactory,
        LoggerInterface $logger
    ) {
        $this->publisherResolver = $publisherResolver;
        $this->eventConfigurationRepository = $eventConfigurationRepository;
        $this->searchCriteriaBuilder = $searchCriteriaBuilder;
        $this->producerConfigurationRepository = $producerConfigurationRepository;
        $this->logger = $logger;
        $this->queueMetadataFactory = $queueMetadataFactory;
        $this->consumerConfigurationRepository = $consumerConfigurationRepository;
        $this->searchCriteriaBuilderFactory = $searchCriteriaBuilderFactory;
        $this->queueItemRepository = $queueItemRepository;
        $this->queueItemFactory = $queueItemFactory;
        $this->messageFactory = $messageFactory;
        $this->statusFactory = $statusFactory;
    }

    /**
     * @param EventInterface|string|EventConfigurationInterface|ConsumerConfigurationInterface $event
     * @param null $argument
     * @throws Exception
     */
    public function emit($event, $argument = null)
    {
        $eventsToEmit = [];
        if (is_a($event, ConsumerConfigurationInterface::class)) {
            //$eventsToEmit = [$event->getOutputEvent()];
            $this->processConsumer($event, $argument);
            return;
        }

        if (is_a($event, EventInterface::class)) {
            $searchCriteria = $this->searchCriteriaBuilder
                ->addFilter(EventConfigurationInterface::BASE_EVENT, $event->getUniqueName())
                ->create();
            $events = $this->eventConfigurationRepository->getList($searchCriteria);
            $eventsToEmit = $events->getItems();
        }

        if (is_a($event, EventConfigurationInterface::class)) {
            $eventsToEmit [] = $event;
        }
        if (\is_string($event)) {
            $searchCriteria = $this->searchCriteriaBuilder
                ->addFilter(EventConfigurationInterface::EVENT_CODE, $event)
                ->create();
            $events = $this->eventConfigurationRepository->getList($searchCriteria);
            $eventsToEmit = $events->getItems();
        }

        /** @var EventConfigurationInterface $eventToEmit */
        foreach ($eventsToEmit as $eventToEmit) {
            $this->processProducers($eventToEmit, $argument);
        }
    }

    /**
     * @param EventConfigurationInterface $eventConfiguration
     * @param $argument
     * @throws Exception
     */
    protected function processProducers(EventConfigurationInterface $eventConfiguration, $argument)
    {
        foreach ($this->getProducersToProcess($eventConfiguration) as $producerConfiguration) {
            $message = null;
            try {
                $producer = $producerConfiguration->getProducer();
                $message = $producer->produce($argument);

                if ($this->isAlreadyInQueue($eventConfiguration, $message)) {
                    continue;
                }

                if ($producer->getStatus()->getCode() === StatusInterface::SKIPPED) {
                    continue;
                }

                if ($producer->getStatus()->getCode() !== StatusInterface::SUCCESS) {
                    $this->writeUnsuccessfulProducerToQueue(
                        $producerConfiguration,
                        $eventConfiguration,
                        $producer->getStatus(),
                        $message
                    );
                    continue;
                }
                $this->publish($eventConfiguration, $message, $producerConfiguration);
            } catch (Exception $exception) {
                /** @var StatusInterface $status */
                $status = $this->statusFactory->create();
                $status->setCode(StatusInterface::ERROR)->setOutputData($exception->getMessage());
                $this->writeUnsuccessfulProducerToQueue(
                    $producerConfiguration,
                    $eventConfiguration,
                    $status,
                    $message ?: $this->messageFactory->create()
                );
                $backtrace = json_encode(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS));
                $this->logger->error($exception->getMessage(), ['exception' => $backtrace]);
            } catch (Throwable $throwable) {
                /** @var StatusInterface $status */
                $status = $this->statusFactory->create();
                $status->setCode(StatusInterface::ERROR)->setOutputData($throwable->getMessage());
                $this->writeUnsuccessfulProducerToQueue(
                    $producerConfiguration,
                    $eventConfiguration,
                    $status,
                    $message ?: $this->messageFactory->create()
                );
                $backtrace = json_encode(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS));
                $this->logger->error($throwable->getMessage() . PHP_EOL . "BACKTRACE => {$backtrace}", ['exception' => $throwable]);
            }
        }
    }

    /**
     * @param EventConfigurationInterface $eventConfiguration
     * @return ProducerConfigurationInterface[]
     */
    protected function getProducersToProcess(EventConfigurationInterface $eventConfiguration): array
    {
        $searchCriteria = $this->searchCriteriaBuilder
            ->addFilter(ProducerConfigurationInterface::OUTPUT_EVENT, $eventConfiguration->getEventCode(), 'eq')
            ->addFilter(ProducerConfigurationInterface::ENABLE, ProducerConfigurationInterface::STATUS_ENABLED, 'eq')
            ->create();

        return $this->producerConfigurationRepository->getList($searchCriteria)->getItems();
    }

    /**
     * @param EventConfigurationInterface $eventConfiguration
     * @param MessageInterface $message
     * @return bool
     */
    protected function isAlreadyInQueue(EventConfigurationInterface $eventConfiguration, MessageInterface $message): bool
    {
        /** @var SearchCriteriaBuilder $searchCriteriaBuilder */
        $searchCriteriaBuilder = $this->searchCriteriaBuilderFactory->create();
        $searchCriteria = $searchCriteriaBuilder
            ->addFilter(QueueItemInterface::STATUS, StatusInterface::PENDING)
            //->addFilter(QueueItemInterface::MESSAGE, $message->getPayload()->toJson())
            ->addFilter(QueueItemInterface::MESSAGE_HASH, sha1($message->getPayload()->toJson()))
            ->addFilter(QueueItemInterface::EVENT, $eventConfiguration->getEventCode())
            ->create();

        return ($this->queueItemRepository->count($searchCriteria) !== 0);
    }

    /**
     *
     * @param ProducerConfigurationInterface $producerConfiguration
     * @param EventConfigurationInterface $eventConfiguration
     * @param StatusInterface $status
     * @param MessageInterface $message
     * @throws Exception
     */
    protected function writeUnsuccessfulProducerToQueue(
        ProducerConfigurationInterface $producerConfiguration,
        EventConfigurationInterface $eventConfiguration,
        StatusInterface $status,
        MessageInterface $message
    ) {
        try {
            $messageHash = sha1($message->getPayload()->toJson());
            /** @var QueueItemInterface $queueItem */
            $queueItem = $this->queueItemFactory->create();
            $queueItem->setPublishedAt(new DateTime());
            $queueItem->setExecutedAt(new DateTime());
            $queueItem->setFrom($producerConfiguration->getUuid());
            $queueItem->setTo('');
            $queueItem->setMessage($message);
            $queueItem->setMessageHash($messageHash);
            $queueItem->setEvent($eventConfiguration->getEventCode());
            $queueItem->setStatus($status);
            $this->queueItemRepository->save($queueItem);
        } catch (Exception $exception) {
            $this->logger->error($exception->getMessage(), ['exception' => $exception]);
        } catch (Throwable $throwable) {
            $this->logger->error($throwable->getMessage(), ['exception' => $throwable]);
        }
    }

    /**
     * @param EventConfigurationInterface $eventConfiguration
     * @param MessageInterface $message
     * @param IdentifiableInterface $from
     * @return ConsumerConfigurationInterface[]
     * @throws NoSuchEntityException
     */
    protected function publish(EventConfigurationInterface $eventConfiguration, MessageInterface $message, IdentifiableInterface $from): array
    {
        /** @var SearchCriteriaBuilder $searchCriteriaBuilder */
        $searchCriteriaBuilder = $this->searchCriteriaBuilderFactory->create();
        $searchCriteria = $searchCriteriaBuilder
            ->addFilter(ConsumerConfigurationInterface::ENABLE, ConsumerConfigurationInterface::STATUS_ENABLED)
            ->addFilter(ConsumerConfigurationInterface::BINDING_EVENT, $eventConfiguration->getEventCode())
            ->create();

        $result = $this->consumerConfigurationRepository->getList($searchCriteria);

        /** @var ConsumerConfigurationInterface $to */
        foreach ($result->getItems() as $to) {
            $publishAt = new \DateTime();
            $executableAfter = clone $publishAt;

            $delay = $to->getTriggerDelay();

            if($delay > 0) {
                $executableAfter->add(new \DateInterval("PT{$delay}S"));
            }

            /** @var QueueMetadataInterface $metadata */
            $metadata = $this->queueMetadataFactory->create();
            $metadata
                ->setFrom($from->getUuid())
                ->setTo($to->getUuid())
                ->setPublishedAt($publishAt)
                ->setExecutableAfter($executableAfter);
            // TODO : Handle publisher hint here right way
            $this->publisherResolver->getDefault()->publish($message, $eventConfiguration->getEventCode(), $metadata);
        }
        return $result->getItems();
    }

    /**
     * @param \FiloBlu\Esb\Api\Data\ConsumerConfigurationInterface $consumerConfiguration
     * @param $argument
     * @return void
     * @throws \Magento\Framework\Exception\NoSuchEntityException
     */
    protected function processConsumer(ConsumerConfigurationInterface $consumerConfiguration, $argument)
    {
        $searchCriteria = $this->searchCriteriaBuilder
            ->addFilter(EventConfigurationInterface::EVENT_CODE, $consumerConfiguration->getOutputEvent(), 'eq')
            ->create();
        $events = $this->eventConfigurationRepository->getList($searchCriteria);

        /** @var EventConfigurationInterface $eventConfiguration */
        foreach ($events->getItems() as $eventConfiguration) {
            if ($this->isAlreadyInQueue($eventConfiguration, $argument)) {
                continue;
            }
            $this->publish($eventConfiguration, $argument, $consumerConfiguration);
        }
    }
}
