<?php

namespace FiloBlu\Flow\Model\Indexer;

use Exception;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Model\Product;
use Magento\CatalogInventory\Api\Data\StockItemInterface;
use Magento\Framework\Api\SearchCriteriaBuilder;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\App\ResourceConnection;
use Magento\Framework\DB\Adapter\AdapterInterface;
use Magento\Framework\EntityManager\MetadataPool;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Indexer\ActionInterface;
use Magento\Framework\Module\Manager;
use Magento\Framework\Mview\ActionInterface as MviewActionInterface;
use Magento\Framework\ObjectManagerInterface;
use Magento\Inventory\Model\ResourceModel\StockSourceLink\Collection;
use Magento\Inventory\Model\StockSourceLink;
use Magento\InventoryApi\Api\Data\SourceItemInterface;
use Magento\InventoryApi\Api\SourceItemRepositoryInterface;
use Magento\InventoryConfigurationApi\Api\GetStockItemConfigurationInterface;
use Magento\InventoryConfigurationApi\Exception\SkuIsNotAssignedToStockException;
use Magento\InventoryReservationsApi\Model\GetReservationsQuantityInterface;
use Psr\Log\LoggerInterface;
use Throwable;
use Magento\CatalogInventory\Api\StockRegistryInterface;

/**
 *
 */
class Stock implements ActionInterface, MviewActionInterface
{
    const INDEXER_ID = 'filoblu_flow_stock';
    const XML_PATH_ENABLE = 'filoblu_flow/flow_stock_indexer/enable';
    const XML_PATH_ENABLE_CONFIGURABLE = 'filoblu_flow/flow_stock_indexer/configurable_enable';

    private $sourceItemRepository;
    /**
     * @var SearchCriteriaBuilder
     */
    private $searchCriteriaBuilder;
    /**
     * @var ResourceConnection
     */
    private $resourceConnection;
    /**
     * @var Product
     */
    private $product;
    /**
     * @var LoggerInterface
     */
    private $logger;
    /**
     * @var GetReservationsQuantityInterface
     */
    private $getReservationsQuantity;
    /**
     * @var GetStockItemConfigurationInterface
     */
    private $getStockItemConfiguration;
    /**
     * @var AdapterInterface
     */
    private $connection;
    /**
     * @var Collection
     */
    private $sourceCollection;
    /**
     * @var  array|null
     */
    private $sourcesToStockId;
    /**
     * @var array|null
     */
    private $stockIdToSources;
    /**
     * @var ScopeConfigInterface
     */
    private $scopeConfig;
    /**
     * @var \Magento\Framework\Module\Manager
     */
    private $moduleManager;
    /**
     * @var \Magento\Framework\EntityManager\MetadataPool
     */
    private $metadataPool;

    private $stockRegistry;

    /**
     * @param SearchCriteriaBuilder $searchCriteriaBuilder
     * @param ObjectManagerInterface $objectManager
     * @param ResourceConnection $resourceConnection
     * @param Product $product
     * @param LoggerInterface $logger
     * @param ScopeConfigInterface $scopeConfig
     * @param \Magento\Framework\Module\Manager $moduleManager
     * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool
     */
    public function __construct(
        SearchCriteriaBuilder $searchCriteriaBuilder,
        ObjectManagerInterface $objectManager,
        ResourceConnection $resourceConnection,
        Product $product,
        LoggerInterface $logger,
        ScopeConfigInterface $scopeConfig,
        Manager $moduleManager,
        MetadataPool $metadataPool,
        StockRegistryInterface $stockRegistry
    ) {
        $this->searchCriteriaBuilder = $searchCriteriaBuilder;

        if (interface_exists(SourceItemRepositoryInterface::class)) {
            $this->sourceItemRepository = $objectManager->get(
                SourceItemRepositoryInterface::class
            );
        }

        if (class_exists(Collection::class)) {
            $this->sourceCollection = $objectManager->get(Collection::class);
        }

        if (interface_exists(GetReservationsQuantityInterface::class)) {
            $this->getReservationsQuantity = $objectManager->get(GetReservationsQuantityInterface::class);
        }

        if (interface_exists(GetStockItemConfigurationInterface::class)) {
            $this->getStockItemConfiguration = $objectManager->get(GetStockItemConfigurationInterface::class);
        }

        $this->resourceConnection = $resourceConnection;
        $this->product = $product;
        $this->logger = $logger;
        $this->scopeConfig = $scopeConfig;
        $this->moduleManager = $moduleManager;
        $this->metadataPool = $metadataPool;
        $this->stockRegistry = $stockRegistry;
    }

    /**
     *
     */
    public function executeFull()
    {
        $this->execute(null);
    }

    /**
     * @param int[] $ids
     */
    public function execute($ids)
    {
        if (!$this->sourceItemRepository) {
            return;
        }

        if (!$this->scopeConfig->getValue(self::XML_PATH_ENABLE)) {
            return;
        }

        try {
            if ($ids) {
                $ids = array_unique($ids);
                $this->searchCriteriaBuilder->addFilter('source_item_id', $ids, 'in');
            }

            $result = $this->sourceItemRepository->getList($this->searchCriteriaBuilder->create());

            foreach ($result->getItems() as $sourceItem) {
                //
                // @see \Magento\InventoryConfiguration\Model\GetLegacyStockItem::execute
                // Stock::DEFAULT_STOCK_ID is used until we have proper multi-stock item configuration
                //
                $stockItemConfiguration = $this->getStockItemConfiguration->execute(
                    $sourceItem->getSku(),
                    \Magento\CatalogInventory\Model\Stock::DEFAULT_STOCK_ID
                );

                // We don't need to Manage Stock
                if (!$stockItemConfiguration->isManageStock() || $stockItemConfiguration->getBackorders()) {
                    continue;
                }

                $this->updateLegacyStockFromSourceItem($sourceItem);
            }

            $this->updateParentStock($ids);
        } catch (Exception $e) {
            $this->logger->critical('FiloBlu_Flow_Stock: ' . $e->getMessage());
        } catch (Throwable $t) {
            $this->logger->critical('FiloBlu_Flow_Stock: ' . $t->getMessage());
        }
    }

    /**
     * @param SourceItemInterface $sourceItem
     * @return int
     * @throws LocalizedException
     * @throws SkuIsNotAssignedToStockException
     */
    public function updateLegacyStockFromSourceItem(SourceItemInterface $sourceItem)
    {
        $sourceCode = $sourceItem->getSourceCode();
        $sku = $sourceItem->getSku();
        $stockIds = $this->getStockId($sourceCode);
        $productId = $this->product->getIdBySku($sourceItem->getSku());

        if (!$productId) {
            $this->logger->alert("Product with sku {$sourceItem->getSku()} not longer exists");
            return null;
        }

        foreach ($stockIds as $stockId) {
            $quantity = 0;
            $stockStatus = SourceItemInterface::STATUS_OUT_OF_STOCK;
            $stockQuantity = $this->getQuantity($sku, $stockId);
            // TODO: fix issue calculating reservation on stockId when one source is shared between two stockId
            $reservations = $this->getReservationsQuantity->execute($sourceItem->getSku(), $stockId);
            $minimumQuantity = $this->getStockItemConfiguration->execute($sourceItem->getSku(), $stockId)->getMinQty();

            $realStockStatus = (int)($stockQuantity + $reservations - $minimumQuantity) > 0 ?
                SourceItemInterface::STATUS_IN_STOCK : SourceItemInterface::STATUS_OUT_OF_STOCK;

            if ($realStockStatus != SourceItemInterface::STATUS_OUT_OF_STOCK) {
                $quantity = $stockQuantity;
                $stockStatus = SourceItemInterface::STATUS_IN_STOCK;
            }

            // For default legacy stock
            if ($sourceItem->getSourceCode() === 'default') {
                $this->updateSourceItem($sourceItem, $stockStatus);
                $this->updateStockItem($productId, $quantity, $stockStatus);
                $this->updateStockStatus($productId, $quantity, $stockStatus);
                continue;
            }

            // For sources != default
            // If product in stock is out of stock we force all sources associated to be out of stock

            if ($stockStatus === SourceItemInterface::STATUS_OUT_OF_STOCK) {
                $this->updateStockSourcesStatus($stockId, $sku, SourceItemInterface::STATUS_OUT_OF_STOCK);
            }

            $this->getConnection()->update(
                $this->getConnection()
                    ->getTableName("inventory_stock_$stockId"),
                [
                    'is_salable' => $stockStatus,
                    'quantity'   => $quantity
                ],
                "sku = '$sku' AND (is_salable != $stockStatus OR quantity != $quantity)"
            );
        }

        return $productId;
    }

    /**
     * @param string $sourceCode
     * @return array
     */
    public function getStockId(string $sourceCode): array
    {
        if ($this->sourcesToStockId === null) {
            $this->createLookupTables();
        }

        return $this->sourcesToStockId[$sourceCode];
    }

    /**
     *
     */
    public function createLookupTables()
    {
        $this->sourcesToStockId = [];
        $this->stockIdToSources = [];

        /** @var StockSourceLink $sourcesToStock */
        foreach ($this->sourceCollection->getItems() as $sourcesToStock) {
            $id = $sourcesToStock->getStockId();
            $code = $sourcesToStock->getSourceCode();
            $this->sourcesToStockId[$code][$id] = $id;
            $this->stockIdToSources[$id][$code] = $code;
        }
    }

    /**
     * @param $sku
     * @param $stockId
     * @return float
     */
    public function getQuantity($sku, $stockId): float
    {
        $sources = implode(
            ',',
            array_map(static function ($a) {
                return "'$a'";
            }, $this->getSourcesFromStockId($stockId))
        );

        return (float)$this->getConnection()
            ->fetchOne(
                "SELECT COALESCE(SUM(quantity),0) AS `quantity` FROM `inventory_source_item` WHERE `sku` = '{$sku}' AND `source_code` IN ({$sources});"
            );
    }

    /**
     * @param $stockId
     * @return mixed
     */
    public function getSourcesFromStockId($stockId)
    {
        if ($this->stockIdToSources === null) {
            $this->createLookupTables();
        }

        return $this->stockIdToSources[$stockId];
    }

    /**
     * @return AdapterInterface
     */
    public function getConnection(): AdapterInterface
    {
        if ($this->connection === null) {
            $this->connection = $this->resourceConnection->getConnection();
        }

        return $this->connection;
    }

    /**
     * @param SourceItemInterface $sourceItem
     * @param int $isInStock
     */
    public function updateSourceItem(SourceItemInterface $sourceItem, int $isInStock): void
    {
        $connection = $this->getConnection();
        $tableName = $connection->getTableName('inventory_source_item');
        $connection->update(
            $tableName,
            ['status' => $isInStock],
            "sku =  '{$sourceItem->getSku()}' AND source_code = '{$sourceItem->getSourceCode()}' AND status != $isInStock"
        );
    }

    /**
     * @param $productId
     * @param $quantity
     * @param $isInStock
     */
    public function updateStockItem($productId, $quantity, $isInStock): void
    {
        $connection = $this->getConnection();
        $tableName = $connection->getTableName('cataloginventory_stock_item');
        $connection->update(
            $tableName,
            [
                'is_in_stock' => $isInStock,
                'qty'         => $quantity
            ],
            "product_id = $productId AND ( is_in_stock != $isInStock OR qty != $quantity )"
        );
    }

    /**
     * @param int $productId
     * @param int $isInStock
     * @param int $quantity
     */
    public function updateStockStatus(int $productId, int $quantity, int $isInStock)
    {
        $connection = $this->getConnection();
        $tableName = $connection->getTableName('cataloginventory_stock_status');
        $connection->update(
            $tableName,
            [
                'stock_status' => $isInStock,
                'qty'          => $quantity
            ],
            "product_id = $productId AND ( stock_status != $isInStock OR qty != $quantity )"
        );
    }

    /**
     * @param $stockId
     * @param $sku
     * @param $status
     */
    public function updateStockSourcesStatus($stockId, $sku, $status)
    {
        $sources = implode(
            ',',
            array_map(static function ($a) {
                return "'{$a}'";
            }, $this->getSourcesFromStockId($stockId))
        );

        $this->getConnection()->update(
            'inventory_source_item',
            [
                'status' => $status
            ],
            "source_code IN ($sources) AND sku = '$sku' AND status != $status"
        );
    }

    /**
     * @param array|null $ids
     * @return void
     * @throws \Exception
     */
    public function updateParentStock($ids)
    {
        $this->updateConfigurable($ids);
    }

    /**
     * @param array|null $ids
     * @return void
     * @throws \Exception
     */
    public function updateConfigurable($ids)
    {
        if (!$this->scopeConfig->isSetFlag(self::XML_PATH_ENABLE_CONFIGURABLE)) {
            return;
        }

        if (!$this->moduleManager->isEnabled('Magento_ConfigurableProduct')) {
            return;
        }

        $linkFiled = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField();

        $connection = $this->getConnection();

        $iss = $connection->getTableName('cataloginventory_stock_status');
        $cpsl = $connection->getTableName('catalog_product_super_link');
        $cpe = $connection->getTableName('catalog_product_entity');
        $is1 = $connection->getTableName('inventory_stock_1');
        $cisi = $connection->getTableName('cataloginventory_stock_item');
        $isi = $connection->getTableName('inventory_source_item');

        $whereCondition = '';

        // FRANCHI-2521
        // Mappo gli $ids da inventory_source_item -> product entity_id
        if (!empty($ids)) {
            $idSelect = $connection->select()->distinct()
                ->from(['isi' => $isi], [])
                ->joinInner(['cpe' => $cpe], 'isi.sku = cpe.sku', ['entity_id'])
                ->where('isi.source_item_id IN (?)', $ids);

            $ids = $connection->fetchCol($idSelect);
        }

        if (!empty($ids)) {
            $whereCondition = ' WHERE b.product_id  IN (  ' . implode(', ', $ids) . ' ) OR a.parent_id IN (' . implode(', ', $ids) . ')';
        }

        // Stock di default dobbiamo aggiornare cataloginventory_stock_status
        $connection->query(
            "INSERT INTO $iss (product_id, website_id ,    stock_id , qty,    stock_status)
 SELECT
    cpe.entity_id as product_id ,
    0 as website_id,
    1 as stock_id,
    0 as qty,
    MAX(isi.is_salable) as is_salable
FROM
    $cpsl a
INNER JOIN $cpsl b ON
    a.parent_id = b.parent_id
INNER JOIN $cpe cpe ON
    a.parent_id = cpe.$linkFiled
INNER JOIN $is1 isi ON
    isi.product_id = a.product_id
$whereCondition
GROUP  BY cpe.entity_id
ON DUPLICATE KEY UPDATE stock_status = VALUES(stock_status)"
        );

        $configurableIds = $ids;
        if (empty($ids)) {
            $configurableIds = $connection->fetchCol(
                $connection->select()->from($cpe, ['entity_id'])->where('type_id = ?', 'configurable')
            );
        }
        $excludedIds = $this->getExcludedConfigurableIds($configurableIds);
        $excludedProductsWhere = '';

        if (!empty($excludedIds)) {
            $whereCondition = ' WHERE b.product_id  NOT IN (  ' . implode(', ', $excludedIds) . ' ) OR a.parent_id IN (' . implode(', ', $ids) . ')';
        }

        $connection->query(
            "INSERT INTO $cisi(product_id, stock_id, is_in_stock)
SELECT
    cpe.entity_id AS product_id,
    1  AS stock_id,
    MAX(isi.is_salable) AS is_in_stock
FROM
    $cpsl a
INNER JOIN $cpsl b ON
    a.parent_id = b.parent_id
INNER JOIN $cpe cpe ON
    a.parent_id = cpe.$linkFiled
INNER JOIN $is1 isi ON
    isi.product_id = a.product_id
GROUP  BY cpe.entity_id
$excludedProductsWhere
ON DUPLICATE KEY UPDATE is_in_stock = VALUES(is_in_stock)"
        );

        // Per stock > 1
        foreach ($this->getStockIds(false) as $stockId) {
            $targetTable = $connection->getTableName("inventory_stock_$stockId");
            $connection->query(
                "INSERT INTO
	$targetTable (sku, quantity, is_salable)
 SELECT
	cpe.sku  AS sku ,
	0 as quantity,
	MAX(isi.is_salable) as is_salable
FROM
	$cpsl a
INNER JOIN $cpsl b ON
	a.parent_id = b.parent_id
INNER JOIN $cpe cpe ON
	a.parent_id = cpe.$linkFiled
INNER JOIN $cpe cpe2 ON
	a.product_id  = cpe2.entity_id
INNER JOIN $targetTable isi ON
	isi.sku = cpe2.sku
$whereCondition
GROUP BY cpe.sku
ON DUPLICATE KEY UPDATE is_salable = VALUES(is_salable)"
            );
        }
    }

    /**
     * return all products ids having managed stock != 1 and backorders
     * @param $ids
     * @return array
     */
    public function getExcludedConfigurableIds($ids)
    {
        $productIds = [];

        foreach ($ids as $id) {
            $stockItem = $this->stockRegistry->getStockItem($id);

            if ($stockItem->getManageStock() != 1 || $stockItem->getBackorders() != StockItemInterface::BACKORDERS_NO) {
                $productIds[] = $id;
            }
        }

        return $productIds;
    }

    /**
     * @param bool $withDefault
     * @return array
     */
    public function getStockIds($withDefault = true)
    {
        $connection = $this->getConnection();

        $select = $connection->select()
            ->from($connection->getTableName('inventory_stock'), ['stock_id']);

        if ($withDefault === false) {
            $select->where('stock_id > 1');
        }
        return $connection->fetchCol($select);
    }

    /**
     * @param array $ids
     */
    public function executeList(array $ids)
    {
        $this->execute($ids);
    }

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