<?php
declare(strict_types=1);

namespace FiloBlu\Refilo\Remote;

use Exception;
use FiloBlu\Refilo\Helper\Indexer;
use FiloBlu\Refilo\Remote\Entity\EntityProviderInterfaceFactory;
use FiloBlu\Refilo\Remote\Entity\EntityProviderReaderHandlerInderface;
use FiloBlu\Refilo\Remote\Helper\TemplateString;
use FiloBlu\Refilo\Remote\Helper\TemplateStringFactory;
use FiloBlu\Refilo\Remote\IndexerConfiguration\IndexerConfigurationInterface;
use FiloBlu\Refilo\Remote\IndexerConfiguration\IndexerConfigurationInterfaceFactory;
use FiloBlu\Refilo\Remote\IndexerConfiguration\IndexerConfigurationRepositoryInterface;
use FiloBlu\Refilo\Remote\IndexerConfiguration\IndexerResolver;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\DataObject;
use Magento\Framework\DataObjectFactory;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\Profiler;
use Magento\Store\Api\Data\StoreInterface;
use Magento\Store\Model\StoreManagerInterface;
use Psr\Log\LoggerInterface;
use RuntimeException;

/**
 * Class AbstractIndexer
 * @package FiloBlu\Refilo\Remote
 */
abstract class AbstractIndexer implements IndexerInterface, EntityProviderReaderHandlerInderface
{
    /**
     * @var EntityProviderInterfaceFactory
     */
    protected $entityProviderFactory;

    /**
     * @var ScopeConfigInterface
     */
    protected $scopeConfig;

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

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

    /**
     * @var StoreManagerInterface
     */
    private $storeManager;

    /**
     * @var string
     */
    private $name;

    /**
     * @var IndexerConfigurationRepositoryInterface
     */
    private $indexerConfigurationRepository;

    /**
     * @var IndexerConfigurationInterface
     */
    private $indexerConfiguration;

    /**
     * @var TemplateStringFactory
     */
    private $templateStringFactory;

    /**
     * @var  TemplateString
     */
    private $remoteIndexTemplate;

    /**
     * @var IndexerConfiguration\IndexerResolver
     */
    private $indexerResolver;

    /**
     * @var IndexerConfiguration\IndexerConfigurationInterfaceFactory
     */
    private $indexerConfigurationFactory;

    /**
     * @var Indexer
     */
    private $indexerHelper;

    /**
     * @var DeletedItemFinderInterface
     */
    private $deletedItemFinder;

    /**
     * AbstractIndexer constructor.
     * @param StoreManagerInterface $storeManager
     * @param EntityProviderInterfaceFactory $entityProviderFactory
     * @param IndexerConfigurationRepositoryInterface $indexerConfigurationRepository
     * @param TemplateStringFactory $templateStringFactory
     * @param IndexerResolver $indexerResolver
     * @param ScopeConfigInterface $scopeConfig
     * @param LoggerInterface $logger
     * @param DataObjectFactory $dataObjectFactory
     * @param IndexerConfigurationInterfaceFactory $indexerConfigurationFactory
     * @param Indexer $indexerHelper
     * @param DeletedItemFinderInterface $deletedItemFinder
     */
    public function __construct(
        StoreManagerInterface                   $storeManager,
        EntityProviderInterfaceFactory          $entityProviderFactory,
        IndexerConfigurationRepositoryInterface $indexerConfigurationRepository,
        TemplateStringFactory                   $templateStringFactory,
        IndexerResolver                         $indexerResolver,
        ScopeConfigInterface                    $scopeConfig,
        LoggerInterface                         $logger,
        DataObjectFactory                       $dataObjectFactory,
        IndexerConfigurationInterfaceFactory    $indexerConfigurationFactory,
        Indexer                                 $indexerHelper,
        DeletedItemFinderInterface              $deletedItemFinder
    )
    {
        $this->entityProviderFactory = $entityProviderFactory;
        $this->indexerConfigurationRepository = $indexerConfigurationRepository;
        $this->templateStringFactory = $templateStringFactory;
        $this->scopeConfig = $scopeConfig;
        $this->logger = $logger;
        $this->indexerResolver = $indexerResolver;
        $this->storeManager = $storeManager;
        $this->dataObjectFactory = $dataObjectFactory;
        $this->indexerConfigurationFactory = $indexerConfigurationFactory;
        $this->indexerHelper = $indexerHelper;
        $this->deletedItemFinder = $deletedItemFinder;
    }

    /**
     * @return Indexer
     */
    public function getIndexerHelper()
    {
        return $this->indexerHelper;
    }

    /**
     *
     */
    public function executeFull()
    {
        $this->executeList([]);
    }

    /**
     * @param array $ids
     */
    public function executeList(array $ids)
    {
        try {
            if (!$this->getIndexerConfiguration()->getEnabled()) {
                return;
            }

            $ids = array_unique($this->beforeExecute($ids));
            $this->executeAction($ids);
            $this->postExecute($ids);
        } catch (Exception $exception) {
            $this->logger->error($exception);
        }
    }

    /**
     * @return IndexerConfigurationInterface
     */
    public function getIndexerConfiguration(): IndexerConfigurationInterface
    {
        if ($this->indexerConfiguration === null) {
            /** @var IndexerConfigurationInterface indexerConfiguration */
            try {
                $this->indexerConfiguration = $this->indexerConfigurationRepository->getByIndexerId($this->getName());
            } catch (NoSuchEntityException $exception) {
                $this->logger->error('Indexer configuration not found', ['exception' => $exception]);
            }
        }

        if ($this->indexerConfiguration === null) {
            $this->indexerConfiguration = $this->indexerConfigurationFactory->create()
                ->setEnabled(false);
        }

        return $this->indexerConfiguration;
    }

    /**
     * @return string
     * @throws NoSuchEntityException
     */
    protected function getName()
    {
        if ($this->name === null) {
            $this->name = $this->indexerResolver->resolve($this->getClass());
        }

        return $this->name;
    }

    /**
     * @return string
     */
    abstract protected function getClass(): string;

    /**
     * @param array $ids
     * @return array
     */
    public function beforeExecute(array $ids = []) :array
    {
        return $ids;
    }

    /**
     * @param array $ids
     * @return mixed
     */
    abstract public function executeAction(array $ids);

    /**
     * @param array $ids
     */
    public function postExecute(array $ids = [])
    {
    }

    /**
     * @param array $ids
     * @return array
     */
    public function getEntitiesToRemove(array $ids = []) : array
    {
        return $this->getDeletedItemFinder()->getDeletedIds(array_unique($ids));
    }

    /**
     * @return StoreManagerInterface
     */
    public function getStoreManager()
    {
        return $this->storeManager;
    }

    /**
     * @param int $id
     * @throws NoSuchEntityException
     */
    public function executeRow($id)
    {
        $this->executeList([$id]);
    }

    /**
     * Invoked by indexer_update_all_views
     *
     * @param int[] $ids
     */
    public function execute($ids)
    {
        $this->executeList($ids);
    }

    /**
     * @return TemplateString
     * @throws NoSuchEntityException
     */
    public function getGetRemoteIndexTemplate(): TemplateString
    {
        if ($this->remoteIndexTemplate) {
            return $this->remoteIndexTemplate;
        }

        $indexName = $this->getIndexerConfiguration()->getIndexName();

        if (empty($indexName)) {
            throw new RuntimeException("Misconfigured Beehive indexer {$this->getName()}");
        }

        $indexName = trim($indexName);

//        $projectId = strtolower($this->scopeConfig->getValue('filoblu/general/project_id'));

        /** @var TemplateString $indexTemplate */
        $this->remoteIndexTemplate = $this->templateStringFactory->create();

        $this->remoteIndexTemplate->withString($indexName);
//          ->withValue('%p', $projectId);

        return $this->remoteIndexTemplate;
    }

    /**
     * @param $entities
     * @param $size
     * @param DataObject $arguments
     * @throws NoSuchEntityException
     */
    public function onRead($entities, $size, DataObject $arguments)
    {
        Profiler::start('FiloBlu_Refilo::AbstractIndexer_Write');
        $this->write($entities, $arguments->getData(self::ARGUMENT_COLLECTION));
        Profiler::stop('FiloBlu_Refilo::AbstractIndexer_Write');
    }

    /**
     * @param $entities
     * @param $collection
     * @throws NoSuchEntityException
     */
    public function write($entities, $collection)
    {
        $this->getConnector()->update($entities, $collection);
    }

    /**
     * @return Connector\ConnectorInterface
     * @throws NoSuchEntityException
     */
    public function getConnector(): Connector\ConnectorInterface
    {
        return $this
            ->getIndexerConfiguration()
            ->getConnectorConfiguration()
            ->getConnector()
            ->setMetadata($this->prepareConnectorMetadata($this->storeManager->getStore()));
    }

    /**
     * @param StoreInterface $store
     * @return DataObject
     */
    public function prepareConnectorMetadata(StoreInterface $store): DataObject
    {
        return $this->dataObjectFactory->create();
    }

    /**
     * @return DeletedItemFinderInterface
     */
    public function getDeletedItemFinder(): DeletedItemFinderInterface
    {
        return $this->deletedItemFinder;
    }

    /**
     * @return \Magento\Framework\App\Config\ScopeConfigInterface
     */
    public function getScopeConfig(): ScopeConfigInterface
    {
        return $this->scopeConfig;
    }

    /**
     * @return LoggerInterface
     */
    protected function getLogger()
    {
        return $this->logger;
    }

    /**
     * @param $provider
     * @param $arguments
     * @return void
     * @throws NoSuchEntityException
     */
    public function walk($provider, $arguments)
    {
        if (!$this->getIndexerConfiguration()->getEnabledMemoryOptimization()) {
            iterator_to_array($provider->attachReaderHandler($this, $arguments));
            return;
        }

        $buffer = [];
        $itemsInBuffer = 0;
        $bulkSize = $this->getIndexerHelper()->getIndexerBulkSize();
        foreach ($provider->toGenerator() as $entity) {
            if ($entity === null) {
                continue;
            }

            $buffer[] = $entity;
            $itemsInBuffer++;
            if ($itemsInBuffer % $bulkSize === 0) {
                $this->onRead($buffer, $itemsInBuffer, $arguments);
                $buffer = [];
                $itemsInBuffer = 0;
            }
        }

        $itemsInBuffer = count($buffer);
        if ($itemsInBuffer > 0 && $itemsInBuffer < $bulkSize) {
            $this->onRead($buffer, $itemsInBuffer, $arguments);
        }
    }
}
