<?php

declare(strict_types=1);

namespace FiloBlu\Refilo\Helper\Catalog;

use InvalidArgumentException;
use Magento\Catalog\Api\CategoryRepositoryInterface;
use Magento\Catalog\Api\Data\CategoryInterface;
use Magento\Catalog\Model\Category;
use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory;
use Magento\Eav\Model\Config;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\App\ResourceConnection;
use Magento\Framework\EntityManager\MetadataPool;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\Serialize\SerializerInterface;
use Magento\Store\Api\Data\StoreInterface;
use Magento\Store\Model\ScopeInterface;
use Magento\Store\Model\StoreManagerInterface;
use FiloBlu\Refilo\Helper\PageBuilderConverter;
use Sabberworm\CSS\Parsing\SourceException;

/**
 * @class CategoryHelper
 * @package FiloBlu\Refilo\Helper
 */
class CategoryHelper
{

    /** @var string */
    public const XML_REFILO_SEARCH_GENERAL_USE_CATEGORY_FULL_PATH = 'refilo_search/general/use_category_full_path';

    /** @var string */
    public const XML_REFILO_SEARCH_GENERAL_CATEGORY_FULL_PATH_SEPARATOR = 'refilo_search/general/category_full_path_separator';

    /** @var string */
    public const XML_REFILO_CATEGORY_BREADCRUMBS_ONLY_ACTIVE_CATEGORY = 'refilo_catalog/categories/breadcrumbs_only_active_category';

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

    /**
     * @var array
     */
    private $categoryCache = [];

    /**
     * @var CollectionFactory
     */
    private $categoryCollectionFactory;

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

    /**
     * @var \Magento\Framework\App\ResourceConnection
     */
    private $resourceConnection;

    /**
     * @var \Magento\Eav\Model\Config
     */
    private $eavConfig;

    /**
     * @var int
     */
    private $isActiveAttributeId;

    /**
     * @var \Magento\Framework\EntityManager\MetadataPool
     */
    private $metadataPool;

    /**
     * @var array
     */
    private $anchorCategories;

    /**
     * @var \Magento\Catalog\Api\CategoryRepositoryInterface
     */
    private $categoryRepository;

    /**
     * @var array
     */
    private $productPositions;

    /**
     * @var array|null
     */
    private $separators;

    /**
     * @var array|null
     */
    private $onlyActiveCategoryFlags;

    /**
     * @var array|null
     */
    private $useFullNameFlag;

    /**
     * @var PageBuilderConverter
     */
    private $pageBuilderConverter;

    /**
     * @var SerializerInterface
     */
    private $serializer;

    /**
     * @var int
     */
    private $refiloSearchWeightAttribute;

    /**
     * Cache of the search weight per category id (entity_id => value, value può essere null).
     * Evita l'N+1: ogni categoria viene interrogata una sola volta nell'intero reindex.
     *
     * @var array
     */
    private $categorySearchWeights = [];

    /**
     * Category id già interrogati (anche quelli senza riga in catalog_category_entity),
     * per non rieseguire la query sulle categorie mancanti.
     *
     * @var array
     */
    private $categorySearchWeightsLoaded = [];

    /**
     * @param ScopeConfigInterface $scopeConfig
     * @param CollectionFactory $categoryCollectionFactory
     * @param CategoryRepositoryInterface $categoryRepository
     * @param ResourceConnection $resourceConnection
     * @param StoreManagerInterface $storeManager
     * @param Config $eavConfig
     * @param MetadataPool $metadataPool
     * @param PageBuilderConverter $pageBuilderConverter
     * @param SerializerInterface $serializer
     */
    public function __construct(
        ScopeConfigInterface $scopeConfig,
        CollectionFactory $categoryCollectionFactory,
        CategoryRepositoryInterface $categoryRepository,
        ResourceConnection $resourceConnection,
        StoreManagerInterface $storeManager,
        Config $eavConfig,
        MetadataPool $metadataPool,
        PageBuilderConverter $pageBuilderConverter,
        SerializerInterface $serializer
    ) {
        $this->scopeConfig = $scopeConfig;
        $this->categoryCollectionFactory = $categoryCollectionFactory;
        $this->storeManager = $storeManager;
        $this->resourceConnection = $resourceConnection;
        $this->eavConfig = $eavConfig;
        $this->metadataPool = $metadataPool;
        $this->categoryRepository = $categoryRepository;
        $this->pageBuilderConverter = $pageBuilderConverter;
        $this->serializer = $serializer;
    }

    /**
     * Used to show full name in the search result
     * @param CategoryInterface| Category $category
     */
    public function getFullName($category): ?string
    {
        if (!$this->getUseFullNameFlag($category->getStore())) {
            return $category->getName();
        }

        $names = [$category->getName()];
        $parent = $category->getParentCategory();

        while (true) {
            if ((int)$parent->getLevel() <= 1 || !$parent->getParentId()) {
                break;
            }

            array_unshift($names, $parent->getName());

            $parent = $parent->getParentCategory();
        }

        return implode($this->getCategorySeparator($category->getStore()), $names);
    }

    /**
     * @param \Magento\Store\Api\Data\StoreInterface $store
     * @return bool
     */
    public function getUseFullNameFlag(StoreInterface $store): bool
    {
        if ($this->useFullNameFlag === null) {
            foreach ($this->storeManager->getStores() as $currentStore) {
                $this->useFullNameFlag[$currentStore->getId()] = $this->scopeConfig->isSetFlag(
                    self::XML_REFILO_SEARCH_GENERAL_USE_CATEGORY_FULL_PATH,
                    ScopeInterface::SCOPE_STORE,
                    $currentStore
                );
            }
        }

        return $this->useFullNameFlag[$store->getId()];
    }

    /**
     * Get Separator for category results in search
     * @param \Magento\Store\Model\Store|\Magento\Store\Api\Data\StoreInterface $store
     * @return void
     */
    public function getCategorySeparator(StoreInterface $store)
    {
        if ($this->separators === null) {
            foreach ($this->storeManager->getStores() as $currentStore) {
                $this->separators[$currentStore->getId()] = $this->scopeConfig->getValue(
                    self::XML_REFILO_SEARCH_GENERAL_CATEGORY_FULL_PATH_SEPARATOR,
                    ScopeInterface::SCOPE_STORE,
                    $currentStore
                ) ?? ' / ';
            }
        }

        return $this->separators[$store->getId()];
    }

    /**
     * @param CategoryInterface| Category | int $category
     * @param $store
     * @return void
     * @throws LocalizedException
     */
    public function getBreadcrumbs($category, $store): array
    {
        if (!isset($this->categoryCache[$store][$category])) {
            $oldStore = $this->storeManager->getStore();
            $this->storeManager->setCurrentStore($this->storeManager->getStore($store));

            $items = $this->categoryCollectionFactory->create()->addAttributeToSelect(
                'is_active'
            )->addAttributeToFilter('level', ['gteq' => 2])->addNameToResult()->joinUrlRewrite()->setStore(
                $store
            )->getItems();

            foreach ($items as $item) {
                $this->categoryCache[$store][$item->getId()] = $item;
            }

            $this->storeManager->setCurrentStore($oldStore);
        }

        if (!isset($this->categoryCache[$store][$category])) {
            return [];
        }

        $currentCategory = $this->categoryCache[$store][$category];

        $paths = explode('/', $currentCategory->getPath());

        $result = [];

        $onlyActiveCategory = $this->getOnlyCategoryFlag($currentCategory->getStore());

        foreach ($paths as $path) {
            if (!isset($this->categoryCache[$store][$path])) {
                continue;
            }

            if ($onlyActiveCategory && !(bool)$this->categoryCache[$store][$path]->getIsActive()) {
                continue;
            }

            $parentCategory = $this->categoryCache[$store][$path];
            $result[] = [
                'label' => $parentCategory->getName(),
                'url'   => $parentCategory->getRequestPath()
            ];
        }

        return $result;
    }

    /**
     * @param \Magento\Store\Api\Data\StoreInterface $store
     * @return void
     */
    public function getOnlyCategoryFlag(StoreInterface $store): bool
    {
        if ($this->onlyActiveCategoryFlags === null) {
            foreach ($this->storeManager->getStores() as $currentStore) {
                $this->onlyActiveCategoryFlags[$currentStore->getId()] = $this->scopeConfig->isSetFlag(
                    self::XML_REFILO_CATEGORY_BREADCRUMBS_ONLY_ACTIVE_CATEGORY,
                    ScopeInterface::SCOPE_STORE,
                    $currentStore
                );
            }
        }

        return $this->onlyActiveCategoryFlags[$store->getId()];
    }

    /**
     * @param $categoryId
     * @return array
     * @throws \Magento\Framework\Exception\LocalizedException
     * @throws \Magento\Framework\Exception\NoSuchEntityException
     */
    public function getIsAnchorCategories($categoryId): array
    {
        if ($this->anchorCategories === null) {
            $categories = $this->categoryCollectionFactory->create()
                ->addAttributeToSelect('is_anchor')
                ->addAttributeToSelect('is_active')
                ->addAttributeToFilter('is_anchor', 1)
                ->addAttributeToFilter('is_active',1)
                ->getItems();

            foreach ($categories as $category) {
                $childrenIds = $this->getChildren($category, true);

                foreach ($childrenIds as $childId) {
                    $this->anchorCategories[$childId] = $this->getParentIdsByChild($childId);
                }
            }
        }

        return $this->anchorCategories[$categoryId] ?? [];
    }

    /**
     * Return children ids of category
     *
     * @param \Magento\Catalog\Model\Category |\Magento\Catalog\Api\Data\CategoryInterface|\Magento\Catalog\Api\Data\CategoryTreeInterface $magentoCategory
     * @param bool $recursive
     * @param bool $isActive
     * @param bool $sortByPosition
     * @return array
     * @throws \Magento\Framework\Exception\LocalizedException
     * @throws Exception
     */
    public function getChildren(
        $magentoCategory,
        bool $recursive = false,
        bool $isActive = true,
        bool $sortByPosition = true
    ): array {
        $connection = $this->resourceConnection->getConnection();
        $linkField = $this->metadataPool->getMetadata(CategoryInterface::class)->getLinkField();
        $attributeId = $this->getIsActiveAttributeId();
        $backendTable = $connection->getTableName('catalog_category_entity_int');
        $checkSql = $connection->getCheckSql('c.value_id > 0', 'c.value', 'd.value');

        $bind = [
            'attribute_id' => $attributeId,
            'store_id'     => $magentoCategory->getStoreId(),
            'scope'        => 1,
            'c_path'       => $magentoCategory->getPath() . '/%',
        ];

        $select = $connection->select()->from(
            ['m' => $this->resourceConnection->getTableName('catalog_category_entity')],
            'entity_id'
        )->joinLeft(
            ['d' => $backendTable],
            "d.attribute_id = :attribute_id AND d.store_id = 0 AND d.{$linkField} = m.{$linkField}", []
        )->joinLeft(
            ['c' => $backendTable],
            "c.attribute_id = :attribute_id AND c.store_id = :store_id AND c.{$linkField} = m.{$linkField}", []
        )->where(
            $connection->quoteIdentifier('path') . ' LIKE :c_path'
        );
        if (!$recursive) {
            $select->where($connection->quoteIdentifier('level') . ' <= :c_level');
            $bind['c_level'] = $magentoCategory->getLevel() + 1;
        }
        if ($isActive) {
            $select->where(
                $checkSql . ' = :scope'
            );
        }
        if ($sortByPosition) {
            $select->order('position ASC');
        }
        $categories = $connection->fetchCol($select, $bind);
        $categoriesIds = [];
        foreach ($categories as $categoryId) {
            $categoriesIds[] = (int)$categoryId;
        }
        return $categoriesIds;
    }

    /**
     * @return int
     * @throws \Magento\Framework\Exception\LocalizedException
     */
    public function getIsActiveAttributeId()
    {
        if ($this->isActiveAttributeId === null) {
            $this->isActiveAttributeId = (int)$this->eavConfig->getAttribute(
                Category::ENTITY,
                'is_active'
            )->getAttributeId();
        }
        return $this->isActiveAttributeId;
    }

    /**
     * @param $categoryId
     * @return array
     * @throws \Magento\Framework\Exception\NoSuchEntityException
     */
    public function getParentIdsByChild($categoryId): array
    {
        /** @var CategoryInterface|Category $category */
        $category = $this->categoryRepository->get($categoryId);

        $output = [];
        foreach ($category->getParentCategories() as $parent) {
            $parentCategory = $this->categoryRepository->get($parent->getEntityId());
            if ($parentCategory->getIsAnchor() == 1) {
                $output[(int)$parent->getEntityId()] = (int)$parent->getPosition();
            }
        }
        return $output;
    }

    /**
     * @param $productId
     * @param $categoryId
     * @return int|mixed
     */
    public function getProductPositionInsideCategory($productId, $categoryId)
    {
        if ($this->productPositions === null) {
            $this->productPositions = [];

            $connection = $this->resourceConnection->getConnection();
            $select = $connection->select()->from($connection->getTableName('catalog_category_product'));
            foreach ($connection->fetchAll($select) as $row) {
                $this->productPositions[$row['product_id']][$row['category_id']] = (int)$row['position'];
            }
        }

        return $this->productPositions[$productId][$categoryId] ?? 999999999;
    }

    /**
     * @param $productId
     * @return mixed
     */
    public function getProductPositions($productId) {
        if ($this->productPositions === null) {
            $this->productPositions = [];
            $connection = $this->resourceConnection->getConnection();
            $select = $connection->select()->from($connection->getTableName('catalog_category_product'));
            foreach ($connection->fetchAll($select) as $row) {
                $this->productPositions[$row['product_id']][$row['category_id']] = (int)$row['position'];
            }
        }

        return $this->productPositions[$productId] ?? [];
    }

    /**
     * @throws SourceException
     */
    public function parseDescription($categoryDescription)
    {
        return $this->pageBuilderConverter->convert($categoryDescription);
    }

    /**
     * @param Category $magentoCategory
     * @return array
     */
    public function getRefiloEmotionalProducts(Category $magentoCategory)
    {
        $emotionalProductsAdapted = [];
        $emotionalProducts = $magentoCategory->getEmotionalProducts();
        if (empty($emotionalProducts)) {
            return null;
        }

        try {
            $emotionalProducts = $this->serializer->unserialize($emotionalProducts);
        } catch (InvalidArgumentException $exception) {
            return null;
        }

        if (!is_array($emotionalProducts) || empty($emotionalProducts['emotional_products'])) {
            return null;
        }

        foreach ($emotionalProducts['emotional_products'] as $emotionalProduct) {
            $emotionalProductsRow['display_page'] = $emotionalProduct['ep_page'];
            $emotionalProductsRow['display_position'] = $emotionalProduct['ep_position'];
            $emotionalProductsRow['columns_spacing'] = $emotionalProduct['ep_column_spacing'];
            $emotionalProductsRow['row_spacing'] = $emotionalProduct['ep_row_spacing'];
            $emotionalProductsAdapted[] = $emotionalProductsRow;
        }

        return $emotionalProductsAdapted;
    }

    /**
     * @return int
     * @throws \Magento\Framework\Exception\LocalizedException
     */
    public function getRefiloSearchWeightAttribute()
    {
        if ($this->refiloSearchWeightAttribute === null) {
            $this->refiloSearchWeightAttribute = (int)$this->eavConfig->getAttribute(
                Category::ENTITY,
                'refilo_search_weight'
            )->getAttributeId();
        }
        return $this->refiloSearchWeightAttribute;
    }

    /**
     * @param array $categoryIds
     * @return array
     * @throws LocalizedException
     * @throws \Exception
     */
    public function getCategorySearchWeight(array $categoryIds)
    {
        $categoryIds = array_values(array_unique(array_map('intval', $categoryIds)));

        // Interroga solo le categorie non ancora viste: ogni categoria viene caricata una
        // sola volta nell'intero reindex, eliminando l'N+1 (prima: una query per prodotto).
        $missing = array_values(array_diff($categoryIds, $this->categorySearchWeightsLoaded));

        if (!empty($missing)) {
            $linkField = $this->metadataPool->getMetadata(CategoryInterface::class)->getLinkField();
            $connection = $this->resourceConnection->getConnection();
            $categoryTable = $connection->getTableName('catalog_category_entity');
            $categoryIntTable = $connection->getTableName('catalog_category_entity_int');
            $searchWeightAttributeId = $this->getRefiloSearchWeightAttribute();

            $select = $connection->select()
                ->from(['cce' => $categoryTable], 'entity_id')
                ->joinLeft(
                    ['ccei' => $categoryIntTable],
                    "cce.$linkField = ccei.$linkField AND ccei.attribute_id = $searchWeightAttributeId",
                    'value'
                )
                ->where('cce.entity_id IN (?)', $missing);

            foreach ($connection->fetchAll($select) as $row) {
                $this->categorySearchWeights[(int)$row['entity_id']] = $row['value'];
            }

            // Marca tutti i richiesti come "visti", incluse le categorie senza riga in cce,
            // così non vengono re-interrogate ad ogni prodotto.
            foreach ($missing as $missingId) {
                $this->categorySearchWeightsLoaded[$missingId] = $missingId;
            }
        }

        // Ricostruisce la stessa struttura del fetchAll originale solo per le categorie esistenti.
        $result = [];
        foreach ($categoryIds as $categoryId) {
            if (array_key_exists($categoryId, $this->categorySearchWeights)) {
                $result[] = [
                    'entity_id' => $categoryId,
                    'value'     => $this->categorySearchWeights[$categoryId],
                ];
            }
        }

        return $result;
    }
}
