<?php
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */
namespace Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav;

use Magento\Catalog\Model\Product\Attribute\Source\Status as ProductStatus;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Api\Data\ProductAttributeInterface;
use Magento\Catalog\Model\ResourceModel\Helper;
use Magento\Eav\Api\AttributeRepositoryInterface;
use Magento\Eav\Model\Config;
use Magento\Framework\Api\SearchCriteriaBuilder;
use Magento\Framework\DB\Select;
use Magento\Framework\DB\Sql\UnionExpression;
use Magento\Framework\EntityManager\MetadataPool;
use Magento\Framework\Event\ManagerInterface;
use Magento\Framework\Indexer\Table\StrategyInterface;
use Magento\Framework\Model\ResourceModel\Db\Context;

/**
 * Catalog Product Eav Select and Multiply Select Attributes Indexer resource model
 *
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
 */
class Source extends AbstractEav
{
    public const TRANSIT_PREFIX = 'transit_';

    /**
     * Catalog resource helper
     *
     * @var \Magento\Catalog\Model\ResourceModel\Helper
     */
    protected $_resourceHelper;

    /**
     * @var \Magento\Eav\Api\AttributeRepositoryInterface
     */
    private $attributeRepository;

    /**
     * @var \Magento\Framework\Api\SearchCriteriaBuilder
     */
    private $criteriaBuilder;

    /**
     * Construct
     *
     * @param Context $context
     * @param StrategyInterface $tableStrategy
     * @param Config $eavConfig
     * @param ManagerInterface $eventManager
     * @param Helper $resourceHelper
     * @param null|string $connectionName
     * @param AttributeRepositoryInterface|null $attributeRepository
     * @param SearchCriteriaBuilder|null $criteriaBuilder
     * @param MetadataPool|null $metadataPool
     */
    public function __construct(
        \Magento\Framework\Model\ResourceModel\Db\Context $context,
        \Magento\Framework\Indexer\Table\StrategyInterface $tableStrategy,
        \Magento\Eav\Model\Config $eavConfig,
        \Magento\Framework\Event\ManagerInterface $eventManager,
        \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper,
        ?string $connectionName = null,
        \Magento\Eav\Api\AttributeRepositoryInterface $attributeRepository = null,
        \Magento\Framework\Api\SearchCriteriaBuilder $criteriaBuilder = null,
        ?\Magento\Framework\EntityManager\MetadataPool $metadataPool = null
    ) {
        parent::__construct(
            $context,
            $tableStrategy,
            $eavConfig,
            $eventManager,
            $connectionName,
            $metadataPool
        );
        $this->_resourceHelper = $resourceHelper;
        $this->attributeRepository = $attributeRepository
            ?: \Magento\Framework\App\ObjectManager::getInstance()
                ->get(\Magento\Eav\Api\AttributeRepositoryInterface::class);
        $this->criteriaBuilder = $criteriaBuilder
            ?: \Magento\Framework\App\ObjectManager::getInstance()
                ->get(\Magento\Framework\Api\SearchCriteriaBuilder::class);
    }

    /**
     * @inheritDoc
     */
    public function reindexEntities($processIds)
    {
        $this->clearTemporaryIndexTable();

        $this->_prepareIndex($processIds);
        $this->_prepareRelationIndex($processIds);

        return $this;
    }

    /**
     * Initialize connection and define main index table
     *
     * @return void
     */
    protected function _construct()
    {
        $this->_init('catalog_product_index_eav', 'entity_id');
    }

    /**
     * Retrieve indexable eav attribute ids
     *
     * @param bool $multiSelect
     * @return array
     */
    protected function _getIndexableAttributes($multiSelect)
    {
        $select = $this->getConnection()->select()->from(
            ['ca' => $this->getTable('catalog_eav_attribute')],
            'attribute_id'
        )->join(
            ['ea' => $this->getTable('eav_attribute')],
            'ca.attribute_id = ea.attribute_id',
            []
        )->where(
            $this->_getIndexableAttributesCondition()
        );

        if ($multiSelect == true) {
            $select->where('ea.backend_type = ?', 'text')->where('ea.frontend_input = ?', 'multiselect');
        } else {
            $select->where('ea.backend_type = ?', 'int')->where('ea.frontend_input IN( ? )', ['select', 'boolean']);
        }

        return $this->getConnection()->fetchCol($select);
    }

    /**
     * Prepare data index for indexable attributes
     *
     * @param array $entityIds the entity ids limitation
     * @param int $attributeId the attribute id limitation
     * @return $this
     */
    protected function _prepareIndex($entityIds = null, $attributeId = null)
    {
        $this->_prepareSelectIndex($entityIds, $attributeId);
        $this->_prepareMultiselectIndex($entityIds, $attributeId);

        return $this;
    }

    /**
     * Prepare data index for indexable select attributes
     *
     * @param array $entityIds the entity ids limitation
     * @param int $attributeId the attribute id limitation
     * @return $this
     * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
     * @throws \Exception
     */
    protected function _prepareSelectIndex($entityIds = null, $attributeId = null)
    {
        $connection = $this->getConnection();
        $idxTable = $this->getIdxTable();
        // prepare select attributes
        $attrIds = $attributeId === null ? $this->_getIndexableAttributes(false) : [$attributeId];
        if (!$attrIds) {
            return $this;
        }
        $productIdField = $this->getMetadataPool()->getMetadata(ProductInterface::class)->getLinkField();
        $attrIdsFlat = implode(',', array_map('intval', $attrIds));
        $ifNullSql = $connection->getIfNullSql('pis.value', 'COALESCE(ds.value, dd.value)');

        /**@var $select Select */
        $select = $connection->select()->distinct(true)->from(
            ['s' => $this->getTable('store')],
            []
        )->joinLeft(
            ['dd' => $this->getTable('catalog_product_entity_int')],
            'dd.store_id = 0',
            []
        )->joinLeft(
            ['ds' => $this->getTable('catalog_product_entity_int')],
            "ds.store_id = s.store_id AND ds.attribute_id = dd.attribute_id AND " .
            "ds.{$productIdField} = dd.{$productIdField}",
            []
        )->joinLeft(
            ['d2d' => $this->getTable('catalog_product_entity_int')],
            sprintf(
                "d2d.store_id = 0 AND d2d.{$productIdField} = dd.{$productIdField} AND d2d.attribute_id = %s",
                $this->_eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, 'status')->getId()
            ),
            []
        )->joinLeft(
            ['d2s' => $this->getTable('catalog_product_entity_int')],
            "d2s.store_id = s.store_id AND d2s.attribute_id = d2d.attribute_id AND " .
            "d2s.{$productIdField} = d2d.{$productIdField}",
            []
        )->joinLeft(
            ['cpe' => $this->getTable('catalog_product_entity')],
            "cpe.{$productIdField} = dd.{$productIdField}",
            []
        )->joinLeft(
            ['pis' => $this->getTable('catalog_product_entity_int')],
            "pis.{$productIdField} = cpe.{$productIdField} " .
            "AND pis.attribute_id = dd.attribute_id AND pis.store_id = s.store_id",
            []
        )->where(
            's.store_id != 0'
        )->where(
            '(ds.value IS NOT NULL OR dd.value IS NOT NULL)'
        )->where(
            (new \Zend_Db_Expr('COALESCE(d2s.value, d2d.value)')) . ' = ' . ProductStatus::STATUS_ENABLED
        )->where(
            "dd.attribute_id IN({$attrIdsFlat})"
        )->where(
            'NOT(pis.value IS NULL AND pis.value_id IS NOT NULL)'
        )->where(
            $ifNullSql . ' IS NOT NULL'
        )->columns(
            [
                'cpe.entity_id',
                'dd.attribute_id',
                's.store_id',
                'value' => new \Zend_Db_Expr('COALESCE(ds.value, dd.value)'),
                'cpe.entity_id AS source_id',
            ]
        );
        $visibilityCondition = $connection->quoteInto(
            '>?',
            \Magento\Catalog\Model\Product\Visibility::VISIBILITY_NOT_VISIBLE
        );
        $this->_addAttributeToSelect(
            $select,
            'visibility',
            "cpe.{$productIdField}",
            's.store_id',
            $visibilityCondition
        );

        if ($entityIds !== null) {
            $ids = implode(',', array_map('intval', $entityIds));
            $selectWithoutDefaultStore = $connection->select()->from(
                ['wd' => $this->getTable('catalog_product_entity_int')],
                [
                    'cpe.entity_id',
                    'attribute_id',
                    'store_id',
                    'value',
                    'cpe.entity_id',
                ]
            )->joinLeft(
                ['cpe' => $this->getTable('catalog_product_entity')],
                "cpe.{$productIdField} = wd.{$productIdField}",
                []
            )->joinLeft(
                ['d2d' => $this->getTable('catalog_product_entity_int')],
                sprintf(
                    "d2d.store_id = 0 AND d2d.{$productIdField} = wd.{$productIdField} AND d2d.attribute_id = %s",
                    $this->_eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, 'status')->getId()
                ),
                []
            )->joinLeft(
                ['d2s' => $this->getTable('catalog_product_entity_int')],
                "d2s.store_id != 0 AND d2s.attribute_id = d2d.attribute_id AND " .
                "d2s.{$productIdField} = d2d.{$productIdField}",
                []
            )
                ->where((new \Zend_Db_Expr('COALESCE(d2s.value, d2d.value)')) . ' = ' . ProductStatus::STATUS_ENABLED)
                ->where("wd.attribute_id IN({$attrIdsFlat})")
                ->where('wd.value IS NOT NULL')
                ->where('wd.store_id != 0')
                ->where("cpe.entity_id IN({$ids})");
            $select->where("cpe.entity_id IN({$ids})");
            $this->_addAttributeToSelect(
                $selectWithoutDefaultStore,
                'visibility',
                "cpe.{$productIdField}",
                'wd.store_id',
                $visibilityCondition
            );

            $selects = new UnionExpression(
                [$select, $selectWithoutDefaultStore],
                Select::SQL_UNION,
                '( %s )'
            );

            $select = $connection->select();
            $select->from(['u' => $selects]);
        }

        /**
         * Add additional external limitation
         */
        $this->_eventManager->dispatch(
            'prepare_catalog_product_index_select',
            [
                'select' => $select,
                'entity_field' => new \Zend_Db_Expr('cpe.entity_id'),
                'website_field' => new \Zend_Db_Expr('s.website_id'),
                'store_field' => new \Zend_Db_Expr('s.store_id'),
            ]
        );
        $query = $select->insertFromSelect($idxTable);
        $connection->query($query);
        return $this;
    }

    /**
     * Prepare data index for indexable multiply select attributes
     *
     * @param array $entityIds the entity ids limitation
     * @param int $attributeId the attribute id limitation
     * @return $this
     * @throws \Exception
     */
    protected function _prepareMultiselectIndex($entityIds = null, $attributeId = null)
    {
        $connection = $this->getConnection();

        // prepare multiselect attributes
        $attrIds = $attributeId === null ? $this->_getIndexableAttributes(true) : [$attributeId];

        if (!$attrIds) {
            return $this;
        }
        $productIdField = $this->getMetadataPool()->getMetadata(ProductInterface::class)->getLinkField();

        // load attribute options
        $options = [];
        $select = $connection->select()->from(
            $this->getTable('eav_attribute_option'),
            ['attribute_id', 'option_id']
        )->where('attribute_id IN(?)', $attrIds);
        $query = $select->query();
        while ($row = $query->fetch()) {
            $options[$row['attribute_id']][$row['option_id']] = true;
        }

        // Retrieve any custom source model options
        $sourceModelOptions = $this->getMultiSelectAttributeWithSourceModels($attrIds);
        $options = array_replace_recursive($options, $sourceModelOptions);

        // prepare get multiselect values query
        $productValueExpression = $connection->getCheckSql('pvs.value_id > 0', 'pvs.value', 'pvd.value');
        $select = $connection->select()->from(
            ['pvd' => $this->getTable('catalog_product_entity_text')],
            []
        )->join(
            ['cs' => $this->getTable('store')],
            '',
            []
        )->joinLeft(
            ['pvs' => $this->getTable('catalog_product_entity_text')],
            "pvs.{$productIdField} = pvd.{$productIdField} AND pvs.attribute_id = pvd.attribute_id"
            . ' AND pvs.store_id=cs.store_id',
            []
        )->joinLeft(
            ['cpe' => $this->getTable('catalog_product_entity')],
            "cpe.{$productIdField} = pvd.{$productIdField}",
            []
        )->where(
            'pvd.store_id=?',
            $connection->getIfNullSql('pvs.store_id', \Magento\Store\Model\Store::DEFAULT_STORE_ID)
        )->where(
            'cs.store_id!=?',
            \Magento\Store\Model\Store::DEFAULT_STORE_ID
        )->where(
            'pvd.attribute_id IN(?)',
            $attrIds
        )->where(
            'cpe.entity_id IS NOT NULL'
        )->columns(
            [
                'entity_id' => 'cpe.entity_id',
                'attribute_id' => 'attribute_id',
                'store_id' => 'cs.store_id',
                'value' => $productValueExpression,
                'source_id' => 'cpe.entity_id',
            ]
        );

        $statusCond = $connection->quoteInto('=?', ProductStatus::STATUS_ENABLED);
        $this->_addAttributeToSelect($select, 'status', "pvd.{$productIdField}", 'cs.store_id', $statusCond);

        if ($entityIds !== null) {
            $select->where('cpe.entity_id IN(?)', $entityIds, \Zend_Db::INT_TYPE);
        }
        /**
         * Add additional external limitation
         */
        $this->_eventManager->dispatch(
            'prepare_catalog_product_index_select',
            [
                'select' => $select,
                'entity_field' => new \Zend_Db_Expr('cpe.entity_id'),
                'website_field' => new \Zend_Db_Expr('cs.website_id'),
                'store_field' => new \Zend_Db_Expr('cs.store_id'),
            ]
        );

        $this->_addAttributeToSelect(
            $select,
            'visibility',
            "cpe.{$productIdField}",
            'cs.store_id',
            $connection->quoteInto('>?', \Magento\Catalog\Model\Product\Visibility::VISIBILITY_NOT_VISIBLE)
        );
        $this->saveDataFromSelect($select, $options);

        return $this;
    }

    /**
     * Get options for multiselect attributes using custom source models
     * Based on @maderlock's fix from:
     * https://github.com/magento/magento2/issues/417#issuecomment-265146285
     *
     * @param array $attrIds
     *
     * @return array
     */
    private function getMultiSelectAttributeWithSourceModels($attrIds)
    {
        // Add options from custom source models
        $this->criteriaBuilder
                ->addFilter('attribute_id', $attrIds, 'in')
                ->addFilter('source_model', true, 'notnull');
        $criteria = $this->criteriaBuilder->create();
        $attributes = $this->attributeRepository->getList(
            ProductAttributeInterface::ENTITY_TYPE_CODE,
            $criteria
        )->getItems();

        $options = [];
        foreach ($attributes as $attribute) {
            $sourceModelOptions = $attribute->getOptions();
            // Add options to list used below
            foreach ($sourceModelOptions as $option) {
                $options[$attribute->getAttributeId()][$option->getValue()] = true;
            }
        }

        return $options;
    }

    /**
     * Save a data to temporary source index table
     *
     * @param array $data
     * @return $this
     */
    protected function _saveIndexData(array $data)
    {
        if (!$data) {
            return $this;
        }
        $connection = $this->getConnection();
        $connection->insertArray(
            $this->getIdxTable(),
            ['entity_id', 'attribute_id', 'store_id', 'value', 'source_id'],
            $data
        );
        return $this;
    }

    /**
     * Retrieve temporary source index table name
     *
     * @param string|null $table
     * @return string
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
     */
    public function getIdxTable($table = null)
    {
        return $this->tableStrategy->getTableName('catalog_product_index_eav');
    }

    /**
     * Save data from select
     *
     * @param Select $select
     * @param array $options
     * @return void
     */
    private function saveDataFromSelect(Select $select, array $options)
    {
        $i = 0;
        $data = [];
        $query = $select->query();
        while ($row = $query->fetch()) {
            $values = isset($row['value']) ? explode(',', $row['value']) : [];
            foreach ($values as $valueId) {
                if (isset($options[$row['attribute_id']][$valueId])) {
                    $data[] = [$row['entity_id'], $row['attribute_id'], $row['store_id'], $valueId, $row['source_id']];
                    $i++;
                    if ($i % 10000 == 0) {
                        $this->_saveIndexData($data);
                        $data = [];
                    }
                }
            }
        }

        $this->_saveIndexData($data);
    }

    /**
     * Prepare data index for product relations
     *
     * @param array $parentIds the parent entity ids limitation
     * @return $this
     */
    protected function _prepareRelationIndex($parentIds = null)
    {
        $connection = $this->getConnection();
        $idxTable = $this->getIdxTable();

        if (!$this->tableStrategy->getUseIdxTable()) {
            $additionalIdxTable = $connection->getTableName(self::TRANSIT_PREFIX . $this->getIdxTable());
            $connection->createTemporaryTableLike($additionalIdxTable, $idxTable);

            $query = $connection->insertFromSelect(
                $this->_prepareRelationIndexSelect($parentIds),
                $additionalIdxTable,
                []
            );
            $connection->query($query);

            $select = $connection->select()->from($additionalIdxTable);
            $query = $connection->insertFromSelect(
                $select,
                $idxTable,
                [],
                \Magento\Framework\DB\Adapter\AdapterInterface::INSERT_IGNORE
            );
            $connection->query($query);

            $connection->dropTemporaryTable($additionalIdxTable);
        } else {
            $query = $connection->insertFromSelect(
                $this->_prepareRelationIndexSelect($parentIds),
                $idxTable,
                [],
                \Magento\Framework\DB\Adapter\AdapterInterface::INSERT_IGNORE
            );
            $connection->query($query);
        }
        return $this;
    }
}
