<?php

namespace FiloBlu\Esb\Model;

use DateTime;
use FiloBlu\Esb\Api\Data\MessageInterface;
use FiloBlu\Esb\Api\Data\MessageInterfaceFactory;
use FiloBlu\Esb\Api\Data\QueueItemInterface;
use FiloBlu\Esb\Api\Data\QueueItemInterfaceFactory;
use FiloBlu\Esb\Api\Data\QueueMetadataInterface;
use FiloBlu\Esb\Api\Data\StatusInterface;
use FiloBlu\Esb\Api\Data\StatusInterfaceFactory;
use FiloBlu\Esb\Api\QueueItemSqlRepositoryInterface;
use Magento\Framework\Api\Search\SearchCriteriaInterfaceFactory;
use Magento\Framework\Api\SearchResultsInterfaceFactory;
use Magento\Framework\App\ResourceConnection;
use Magento\Framework\DataObject;
use Throwable;

/**
 * Class QueueItemRepository
 * @package FiloBlu\Esb\Model
 */
class QueueItemRepository extends AbstractSqlRepository implements QueueItemSqlRepositoryInterface
{

    /**
     * @var QueueItemInterfaceFactory
     */
    private $queueItemFactory;

    /**
     * @var MessageInterfaceFactory
     */
    private $messageFactory;

    /**
     * @var StatusInterfaceFactory
     */
    private $statusInterfaceFactory;

    /**
     * QueueItemRepository constructor.
     * @param ResourceConnection $resourceConnection
     * @param SearchCriteriaInterfaceFactory $searchCriteriaFactory
     * @param SearchResultsInterfaceFactory $searchResultsFactory
     * @param QueueItemInterfaceFactory $queueItemFactory
     * @param MessageInterfaceFactory $messageFactory
     * @param StatusInterfaceFactory $statusInterfaceFactory
     */
    public function __construct(
        ResourceConnection $resourceConnection,
        SearchCriteriaInterfaceFactory $searchCriteriaFactory,
        SearchResultsInterfaceFactory $searchResultsFactory,
        QueueItemInterfaceFactory $queueItemFactory,
        MessageInterfaceFactory $messageFactory,
        StatusInterfaceFactory $statusInterfaceFactory
    ) {
        parent::__construct($resourceConnection, $searchCriteriaFactory, $searchResultsFactory);
        $this->queueItemFactory = $queueItemFactory;
        $this->messageFactory = $messageFactory;
        $this->statusInterfaceFactory = $statusInterfaceFactory;
    }

    /**
     * @param array $row
     * @return QueueItemInterface
     */
    public function fromRow(array $row)
    {
        $publishedAt = DateTime::createFromFormat(self::DATE_FORMAT, $row[QueueMetadataInterface::PUBLISHED_AT] ?? '');

        if (!$publishedAt) {
            $publishedAt = null;
        }

        $executableAfter = DateTime::createFromFormat(
            self::DATE_FORMAT,
            $row[QueueMetadataInterface::EXECUTABLE_AFTER] ?? ''
        );

        if (!$executableAfter) {
            $executableAfter = null;
        }

        $executedAt = DateTime::createFromFormat(self::DATE_FORMAT, $row[QueueMetadataInterface::EXECUTED_AT] ?? '');

        if (!$executedAt) {
            $executedAt = null;
        }

        $finishedAt = DateTime::createFromFormat(self::DATE_FORMAT, $row[QueueMetadataInterface::FINISHED_AT] ?? '');

        if (!$finishedAt) {
            $finishedAt = null;
        }

        $queueItem = $this->queueItemFactory->create();

        $status = $this->statusInterfaceFactory->create();

        $status->setCode($row[QueueItemInterface::STATUS])->setOutputData(
                $row[StatusInterface::STATUS_OUTPUT]
            )->setInputData($row[StatusInterface::STATUS_INPUT]);

        /** @var MessageInterface $message */
        $message = $this->messageFactory->create();
        $message->setPayload(new DataObject(json_decode($row[QueueItemInterface::MESSAGE], true)));

        /** @var QueueItemInterface $queueItem */
        $queueItem
            ->setId($row[QueueItemInterface::ID])
            ->setRetryCount($row[QueueItemInterface::RETRY_COUNT])
            ->setMessage($message)
            ->setMessageHash($row[QueueItemInterface::MESSAGE_HASH])
            ->setStatus($status)->setRetryItemId(
                $row[QueueItemInterface::RETRY_ITEM_ID]
            )->setEvent($row[QueueItemInterface::EVENT])->setPriority($row[QueueItemInterface::PRIORITY])->setTo(
                $row[QueueMetadataInterface::TO]
            )->setFrom($row[QueueMetadataInterface::FROM])->setExecutableAfter($executableAfter)->setExecutedAt(
                $executedAt
            )->setFinishedAt($finishedAt)->setPublishedAt($publishedAt);

        return $queueItem;
    }

    /**
     * @return string
     */
    public function getIdFieldName(): string
    {
        return QueueItemInterface::ID;
    }

    /**
     * @param QueueItemInterface $item
     * @return QueueItemInterface
     */
    public function save(QueueItemInterface $item)
    {
        $connection = $this->resourceConnection->getConnection();
        $table = $connection->getTableName(QueueItemSqlRepositoryInterface::TABLE);

        $bindings = [
            QueueItemInterface::EVENT                => $item->getEvent(),
            QueueItemInterface::PRIORITY             => $item->getPriority(),
            QueueItemInterface::MESSAGE              => $item->getMessage()->getPayload() ? $item->getMessage(
            )->getPayload()->toJson() : '{}',
            QueueItemInterface::MESSAGE_HASH        => $item->getMessageHash(),
            QueueMetadataInterface::EXECUTABLE_AFTER => $item->getExecutableAfter() ? $item->getExecutableAfter(
            )->format(
                self::DATE_FORMAT
            ) : null,
            QueueMetadataInterface::FINISHED_AT      => $item->getFinishedAt() ? $item->getFinishedAt()->format(
                self::DATE_FORMAT
            ) : null,
            QueueMetadataInterface::EXECUTED_AT      => $item->getExecutedAt() ? $item->getExecutedAt()->format(
                self::DATE_FORMAT
            ) : null,
            QueueMetadataInterface::PUBLISHED_AT     => $item->getPublishedAt() ? $item->getPublishedAt()->format(
                self::DATE_FORMAT
            ) : null,
            QueueItemInterface::RETRY_COUNT          => $item->getRetryCount(),
            QueueItemInterface::RETRY_ITEM_ID        => $item->getRetryItemId(),
            QueueMetadataInterface::FROM             => $item->getFrom(),
            QueueMetadataInterface::TO               => $item->getTo(),
            QueueItemInterface::STATUS               => $item->getStatus() ? $item->getStatus()->getCode() : 'pending',
            StatusInterface::STATUS_INPUT            => $item->getStatus()->getInputData(),
            StatusInterface::STATUS_OUTPUT           => $item->getStatus()->getOutputData()

        ];

        if ($item->getId()) {
            $connection->update(
                $table,
                $bindings,
                $connection->quoteInto(
                    sprintf('%s = ?', QueueItemInterface::ID),
                    $item->getId()
                )
            );
        } else {
            $connection->insert($table, $bindings);
            $item->setId($connection->lastInsertId($table));
        }

        return $item;
    }

    /**
     * @param QueueItemInterface $item
     * @return void
     */
    public function delete(QueueItemInterface $item)
    {
        if (!($id = $item->getId())) {
            return;
        }

        $connection = $this->resourceConnection->getConnection();
        $table = $connection->getTableName(QueueItemSqlRepositoryInterface::TABLE);

        $connection->delete($table, [sprintf('%s = ?', QueueItemInterface::ID) => $id]);
    }

    /**
     * @inheritDoc
     */
    public function getTable(): string
    {
        return self::TABLE;
    }

    /**
     * @inheritDoc
     */
    public function isPending(QueueItemInterface $item): bool
    {
        $queueItemFieldId = QueueItemInterface::ID;
        $queueItemFieldStatus = QueueItemInterface::STATUS;
        $tableName =  $this->resourceConnection->getTableName(QueueItemSqlRepositoryInterface::TABLE);
        $statusPending = StatusInterface::PENDING;
        $connection = $this->resourceConnection->getConnection();
        $connection->beginTransaction();
        $sql = "SELECT $queueItemFieldId FROM $tableName WHERE $queueItemFieldId = {$item->getId()} AND $queueItemFieldStatus = '$statusPending' FOR UPDATE";

        try {
            $result = $connection->query($sql)->fetch();
            if ($result === false) {
                $connection->commit();
                return false;
            }
            $connection->update(
                $tableName,
                [$queueItemFieldStatus => StatusInterface::PROCESSING],
                "$queueItemFieldId = {$item->getId()}"
            );
            $connection->commit();
            return true;
        } catch (Throwable $e) {
            $connection->rollBack();
            return false;
        }
    }
}
