<?php

namespace FiloBlu\Esb\Model;

use Exception;
use FiloBlu\Esb\Api\ConsumerConfigurationRepositoryInterface;
use FiloBlu\Esb\Api\ConsumerResolverInterface;
use FiloBlu\Esb\Api\Data\ConsumerConfigurationInterface;
use FiloBlu\Esb\Api\Data\ConsumerConfigurationInterfaceFactory;
use FiloBlu\Esb\Api\Data\ConsumerInterface;
use FiloBlu\Esb\Api\Data\IdentifiableInterface;
use FiloBlu\Esb\Api\RetryStrategyResolverInterface;
use Magento\Framework\Api\Search\SearchCriteriaInterfaceFactory;
use Magento\Framework\Api\SearchResultsInterfaceFactory;
use Magento\Framework\App\ResourceConnection;
use Magento\Framework\DataObjectFactory;
use Magento\Framework\Exception\NoSuchEntityException;
use Zend_Db;

use function get_class;
use function is_string;

/**
 * Class ConsumerConfigurationRepository
 * @package FiloBlu\Esb\Model
 */
class ConsumerConfigurationRepository extends AbstractSqlRepository implements ConsumerConfigurationRepositoryInterface
{
    /**
     * @var string
     */
    const TABLE = 'esb_consumer_configuration';

    /**
     * @var ConsumerConfigurationInterfaceFactory
     */
    private $consumerConfigurationFactory;

    /**
     * @var DataObjectFactory
     */
    private $dataObjectFactory;

    /**
     * @var RetryStrategyResolverInterface
     */
    private $strategyResolver;
    /**
     * @var ObjectTypeLoaderResolverInterface
     */
    private $loaderResolver;
    /**
     * @var ObjectTypeSaverResolverInterface
     */
    private $saverResolver;
    /**
     * @var ConsumerResolverInterface
     */
    private $consumerResolver;

    /**
     * ConsumerConfigurationRepository constructor.
     * @param ResourceConnection $resourceConnection
     * @param SearchCriteriaInterfaceFactory $searchCriteriaFactory
     * @param SearchResultsInterfaceFactory $searchResultsFactory
     * @param ConsumerConfigurationInterfaceFactory $consumerConfigurationFactory
     * @param RetryStrategyResolverInterface $strategyResolver
     * @param DataObjectFactory $dataObjectFactory
     * @param ObjectTypeLoaderResolverInterface $loaderResolver
     * @param ObjectTypeSaverResolverInterface $saverResolver
     * @param ConsumerResolverInterface $consumerResolver
     */
    public function __construct(
        ResourceConnection $resourceConnection,
        SearchCriteriaInterfaceFactory $searchCriteriaFactory,
        SearchResultsInterfaceFactory $searchResultsFactory,
        ConsumerConfigurationInterfaceFactory $consumerConfigurationFactory,
        RetryStrategyResolverInterface $strategyResolver,
        DataObjectFactory $dataObjectFactory,
        ObjectTypeLoaderResolverInterface $loaderResolver,
        ObjectTypeSaverResolverInterface $saverResolver,
        ConsumerResolverInterface $consumerResolver
    ) {
        parent::__construct($resourceConnection, $searchCriteriaFactory, $searchResultsFactory);
        $this->consumerConfigurationFactory = $consumerConfigurationFactory;
        $this->strategyResolver = $strategyResolver;
        $this->dataObjectFactory = $dataObjectFactory;
        $this->loaderResolver = $loaderResolver;
        $this->saverResolver = $saverResolver;
        $this->consumerResolver = $consumerResolver;
    }

    /**
     * @param ConsumerConfigurationInterface $configuration
     * @return \FiloBlu\Esb\Api\Data\ConsumerConfigurationInterface
     */
    public function save(ConsumerConfigurationInterface $configuration)
    {
        $connection = $this->resourceConnection->getConnection();
        $table = $connection->getTableName(self::TABLE);

        $consumer = $configuration->getConsumer();

        $bindings = [
            IdentifiableInterface::ID                                    => $configuration->getId(),
            ConsumerConfigurationInterface::CONSUMER                     => $consumer->getUniqueName(),
            ConsumerConfigurationInterface::OUTPUT_EVENT                 => $configuration->getOutputEvent(),
            ConsumerConfigurationInterface::BINDING_EVENT                => $configuration->getBindingEvent(),
            ConsumerConfigurationInterface::PARAMETERS                   => $configuration->getParameters(
            )->convertToJson(),
            ConsumerConfigurationInterface::LABEL                        => $configuration->getLabel(),
            ConsumerConfigurationInterface::ENABLE                       => $configuration->isEnabled(),
            ConsumerConfigurationInterface::RETRY_STRATEGY               => get_class(
                $configuration->getRetryStrategy()
            ),
            IdentifiableInterface::UUID                                  => $configuration->getUuid(),
            ConsumerConfigurationInterface::PRIORITY                     => $configuration->getPriority(),
            ConsumerConfigurationInterface::TRIGGER_DELAY                => $configuration->getTriggerDelay(),
            ConsumerConfigurationInterface::SAVE_RESPONSE                => $configuration->getSaveResponse(),
            ConsumerConfigurationInterface::SAVE_REQUEST                 => $configuration->getSaveRequest(),
            ConsumerConfigurationInterface::RETRY_STRATEGY_CONFIGURATION => $configuration->getRetryStrategyConfiguration(
            ),
            ConsumerConfigurationInterface::OBJECT_SAVER                 => $configuration->getObjectSaver(
            )->getUniqueName(),
            ConsumerConfigurationInterface::OBJECT_LOADER                => $configuration->getObjectLoader(
            )->getUniqueName()
        ];

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

        return $configuration;
    }

    /**
     * @inheritDoc
     * @throws NoSuchEntityException
     */
    public function getByConsumer($consumer): ConsumerConfigurationInterface
    {
        if (is_string($consumer)) {
            $consumerClass = $consumer;
        } elseif ($consumer instanceof ConsumerInterface) {
            $consumerClass = get_class($consumer);
        } else {
            throw new NoSuchEntityException(__('Unable to find configuration for consumer'));
        }

        $consumerClass = trim($consumerClass, '\\');

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

        $select = $connection->select()->from($table)->where(
            sprintf('%s = :consumer', ConsumerConfigurationInterface::CONSUMER)
        );

        $item = $connection->fetchRow($select, ['consumer' => $consumerClass], Zend_Db::FETCH_ASSOC);

        if (empty($item)) {
            throw new NoSuchEntityException(__("Could not find consumer item with id = {$consumerClass}"));
        }

        return $this->fromRow($item);
    }

    /**
     * @param array $row
     * @return ConsumerConfigurationInterface
     * @throws NoSuchEntityException
     */
    public function fromRow(array $row)
    {
        /** @var ConsumerConfigurationInterface $configuration */
        $configuration = $this->consumerConfigurationFactory->create();

        $parameters = $this->dataObjectFactory->create();

        if ($row[ConsumerConfigurationInterface::PARAMETERS]) {
            $parameters->setData(json_decode($row[ConsumerConfigurationInterface::PARAMETERS], true));
        }

        $consumer = $this->consumerResolver->get($row[ConsumerConfigurationInterface::CONSUMER]);

        if ($this->saverResolver->has($row[ConsumerConfigurationInterface::OBJECT_SAVER])) {
            $objectSaver = $this->saverResolver->get($row[ConsumerConfigurationInterface::OBJECT_SAVER]);
        } else {
            $objectSaver = $this->saverResolver->getFor($consumer->getObjectTypeDescriptor());
        }

        if ($this->loaderResolver->has($row[ConsumerConfigurationInterface::OBJECT_LOADER])) {
            $objectLoader = $this->loaderResolver->get($row[ConsumerConfigurationInterface::OBJECT_LOADER]);
        } else {
            $objectLoader = $this->loaderResolver->getFor($consumer->getObjectTypeDescriptor());
        }

        $configuration
            ->setObjectSaver($objectSaver)
            ->setObjectLoader($objectLoader)
            ->setSaveResponse($row[ConsumerConfigurationInterface::SAVE_RESPONSE] ?? false)
            ->setSaveRequest($row[ConsumerConfigurationInterface::SAVE_REQUEST] ?? false)
            ->setPriority($row[ConsumerConfigurationInterface::PRIORITY])
            ->setRetryStrategyConfiguration($row[ConsumerConfigurationInterface::RETRY_STRATEGY_CONFIGURATION])
            ->setBindingEvent($row[ConsumerConfigurationInterface::BINDING_EVENT])
            ->setOutputEvent($row[ConsumerConfigurationInterface::OUTPUT_EVENT])
            ->setTriggerDelay($row[ConsumerConfigurationInterface::TRIGGER_DELAY]??0)
            ->setConsumer($consumer)
            ->setParameters($parameters)
            ->setLabel($row[ConsumerConfigurationInterface::LABEL] ?? '')
            ->setUuid($row[IdentifiableInterface::UUID])
            ->setId($row[IdentifiableInterface::ID]);
        try {
            $retryStrategy = $this->strategyResolver->get($row[ConsumerConfigurationInterface::RETRY_STRATEGY]);
        } catch (Exception $exception) {
            $retryStrategy = $this->strategyResolver->getDefault();
        }

        $configuration->setRetryStrategy($retryStrategy);

        if ($row[ConsumerConfigurationInterface::ENABLE]) {
            $configuration->enable();
        } else {
            $configuration->disable();
        }

        return $configuration;
    }

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

        $this->deleteById($id);
    }

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

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