<?php

namespace FiloBlu\Esb\Model;

use DateTime;
use Exception;
use FiloBlu\Esb\Api\ConsumerConfigurationRepositoryInterface;
use FiloBlu\Esb\Api\Data\ConsumerConfigurationInterface;
use FiloBlu\Esb\Api\Data\IdentifiableInterface;
use FiloBlu\Esb\Api\Data\QueueItemInterface;
use FiloBlu\Esb\Api\Data\QueueMetadataInterface;
use FiloBlu\Esb\Api\Data\QueueMetadataInterfaceFactory;
use FiloBlu\Esb\Api\Data\QueueStateHandlerInterface;
use FiloBlu\Esb\Api\Data\StatusInterface;
use FiloBlu\Esb\Api\DispatcherInterface;
use FiloBlu\Esb\Api\QueueItemRepositoryInterface;
use FiloBlu\Esb\Core\Exception\NonRecoverableException;
use FiloBlu\Esb\Framework\Event\EventEmitter;
use Magento\Framework\Api\SearchCriteriaBuilder;
use Magento\Framework\Api\SearchCriteriaBuilderFactory;
use Magento\Framework\Api\SortOrder;
use Magento\Framework\Api\SortOrderBuilder;
use Psr\Log\LoggerInterface;
use Throwable;

/**
 * Class Dispatcher
 * @package FiloBlu\Esb\Model
 */
class Dispatcher implements DispatcherInterface
{
    /**
     * @var QueueItemRepositoryInterface
     */
    protected $queueItemRepository;
    /**
     * @var SearchCriteriaBuilderFactory
     */
    protected $searchCriteriaBuilderFactory;
    /**
     * @var LoggerInterface
     */
    protected $logger;
    /**
     * @var ConsumerConfigurationRepositoryInterface
     */
    private $consumerConfigurationRepository;

    /**
     * @var SortOrderBuilder
     */
    private $sortOrderBuilder;
    /**
     * @var EventEmitter
     */
    private $eventEmitter;

    /**
     * @param \Psr\Log\LoggerInterface $logger
     * @param \FiloBlu\Esb\Api\QueueItemRepositoryInterface $queueItemRepository
     * @param \Magento\Framework\Api\SearchCriteriaBuilderFactory $searchCriteriaBuilderFactory
     * @param \FiloBlu\Esb\Api\ConsumerConfigurationRepositoryInterface $consumerConfigurationRepository
     * @param \Magento\Framework\Api\SortOrderBuilder $sortOrderBuilder
     * @param \FiloBlu\Esb\Framework\Event\EventEmitter $eventEmitter
     */
    public function __construct(
        LoggerInterface $logger,
        QueueItemRepositoryInterface $queueItemRepository,
        SearchCriteriaBuilderFactory $searchCriteriaBuilderFactory,
        ConsumerConfigurationRepositoryInterface $consumerConfigurationRepository,
        SortOrderBuilder $sortOrderBuilder,
        EventEmitter $eventEmitter
    ) {
        $this->searchCriteriaBuilderFactory = $searchCriteriaBuilderFactory;
        $this->queueItemRepository = $queueItemRepository;
        $this->logger = $logger;
        $this->consumerConfigurationRepository = $consumerConfigurationRepository;
        $this->sortOrderBuilder = $sortOrderBuilder;
        $this->eventEmitter = $eventEmitter;
    }

    /**
     *
     * @throws Exception
     */
    public function dispatch()
    {
        /** @var SearchCriteriaBuilder $builder */
        $builder = $this->searchCriteriaBuilderFactory->create();
        $builder->addFilter(QueueItemInterface::STATUS, StatusInterface::PENDING);
        $builder->addFilter(
            QueueMetadataInterface::EXECUTABLE_AFTER,
            (new DateTime())->format(QueueItemRepositoryInterface::DATE_FORMAT),
            'lteq'
        );

        $builder->setSortOrders($this->getSortOrders());
        // TODO add a configuration limit of item to be processed
        // $builder->setPageSize(20);

        $result = $this->queueItemRepository->getList($builder->create());

        /** @var QueueItemInterface $item */
        foreach ($result->getItems() as $item) {
            // isPending execute a lock on MySQL, prevent concurrent executions
            if (!$this->queueItemRepository->isPending($item)) {
                continue;
            }

            // Dispatch to all consumers
            foreach ($this->getConsumersToProcess($item) as $consumerConfiguration) {
                //$consumerConfiguration = $this->getConsumersToProcess($item);
                $status = $item->getStatus();

                try {
                    //The processing status is set from $this->queueItemRepository->isPending($item)
                    $this->setProcessing($item);
                    $consumer = $consumerConfiguration->getConsumer();
                    $message = $consumer->consume(clone $item->getMessage());

                    $outputEvent = $consumerConfiguration->getOutputEvent();

                    $status = $consumer->getStatus();
                    $item->setStatus($status);

                    switch ($status->getCode()) {
                        case StatusInterface::SUCCESS:
                            // TODO : this must be done into a validator
                            if ($outputEvent && $outputEvent !== $item->getEvent()) {
                                $this->eventEmitter->emit($consumerConfiguration, $message);
                            }
                            break;
                        case StatusInterface::SKIPPED:
                            break;
                        default:
                            $status = $this->handleError($item, $consumerConfiguration);
                    }
                } catch (NonRecoverableException $nonRecoverableException) {
                    $status->setOutputData($nonRecoverableException->getMessage())->setCode(StatusInterface::ERROR);
                } catch (Exception $exception) {
                    $status = $this->handleFailure($item, $consumerConfiguration, $exception);
                } catch (Throwable $throwable) {
                    $status = $this->handleFailure($item, $consumerConfiguration, $throwable);
                } finally {
                    $item->setFinishedAt(new DateTime());
                    $item->setStatus($status);
                    $this->queueItemRepository->save($item);
                }
            }
        }
    }

    /**
     * @return SortOrder[]
     */
    public function getSortOrders(): array
    {
        $sortOrders = [];
        $sortOrders[] = $this->sortOrderBuilder
            ->setDirection(SortOrder::SORT_ASC)
            ->setField(QueueMetadataInterface::EXECUTABLE_AFTER)
            ->create();
        $sortOrders[] = $this->sortOrderBuilder
            ->setDirection(SortOrder::SORT_ASC)
            ->setField(QueueMetadataInterface::PUBLISHED_AT)
            ->create();
        $sortOrders[] = $this->sortOrderBuilder
            ->setDirection(SortOrder::SORT_ASC)
            ->setField(QueueItemInterface::PRIORITY)
            ->create();
        return $sortOrders;
    }

    /**
     * @param QueueItemInterface $queueItem
     * @return ConsumerConfigurationInterface[]
     */
    protected function getConsumersToProcess(QueueItemInterface $queueItem): array
    {
        /** @var SearchCriteriaBuilder $searchCriteriaBuilder */
        $searchCriteriaBuilder = $this->searchCriteriaBuilderFactory->create();
        $searchCriteria = $searchCriteriaBuilder
            ->addFilter(ConsumerConfigurationInterface::ENABLE, ConsumerConfigurationInterface::STATUS_ENABLED)
            ->addFilter(ConsumerConfigurationInterface::BINDING_EVENT, $queueItem->getEvent())
            ->addFilter(IdentifiableInterface::UUID, $queueItem->getTo())
            ->create();

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

    /**
     * @param QueueItemInterface $queueItem
     * @throws Exception
     */
    protected function setProcessing(QueueItemInterface $queueItem)
    {
        $queueItem->getStatus()->setCode(StatusInterface::PROCESSING);
        $queueItem->setExecutedAt(new DateTime());
        $this->queueItemRepository->save($queueItem);
    }

    /**
     * @param QueueItemInterface $item
     * @param ConsumerConfigurationInterface $consumerConfiguration
     * @return StatusInterface
     */
    protected function handleError(QueueItemInterface $item, ConsumerConfigurationInterface $consumerConfiguration)
    {
        $id = $item->getId();

        if ($consumerConfiguration->getRetryStrategy()->reschedule($item)) {
            $message = "Rescheduled item with id $id because an error occurred";
        } else {
            $this->dispatchQueueItemStateHandler($item, $consumerConfiguration);
            $message = "Item with $id will not be scheduled anymore";
        }

        $this->logger->error($message);

        return $item->getStatus()->setCode(StatusInterface::ERROR);
    }

    /**
     * @param QueueItemInterface $item
     * @param ConsumerConfigurationInterface $consumerConfiguration
     */
    protected function dispatchQueueItemStateHandler(
        QueueItemInterface $item,
        ConsumerConfigurationInterface $consumerConfiguration
    ) {
        if ($consumerConfiguration->getConsumer() instanceof QueueStateHandlerInterface) {
            /** @var \FiloBlu\Esb\Api\Data\QueueStateHandlerInterface $consumer */
            $consumer = $consumerConfiguration->getConsumer();
            try {
                $consumer->onFailure($item);
            } catch (Throwable $t) {
                $this->logger->error($t->getMessage(), ['exception' => $t]);
            }
        }
    }

    /**
     * @param QueueItemInterface $item
     * @param ConsumerConfigurationInterface $consumerConfiguration
     * @param Throwable $exception
     * @return StatusInterface
     */
    protected function handleFailure(
        QueueItemInterface $item,
        ConsumerConfigurationInterface $consumerConfiguration,
        Throwable $exception
    ): StatusInterface {
        $id = $item->getId();

        $message = $exception->getMessage();
        $item->getStatus()->appendOutputData($exception->getMessage())->setCode(StatusInterface::ERROR);

        if ($consumerConfiguration->getRetryStrategy()->reschedule($item)) {
            $message = "Rescheduled item with id $id because exception : $message";
        } else {
            $this->dispatchQueueItemStateHandler($item, $consumerConfiguration);
            $message = "Item with $id will not be scheduled anymore : $message";
        }

        $this->logger->error($message, ['exception' => $exception]);

        return $item->getStatus();
    }
}
