<?php

namespace FiloBlu\Refilo\Helper\Catalog;

use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Model\Product\Attribute\Source\Status;
use Magento\Catalog\Model\ResourceModel\Eav\Attribute;
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory;
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
use Magento\Eav\Api\AttributeRepositoryInterface;
use Magento\Framework\Api\SearchCriteriaBuilder;
use Magento\Framework\Api\SortOrder;
use Magento\Framework\Api\SortOrderBuilder;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Store\Api\Data\StoreInterface;
use Magento\Store\Model\StoreManagerInterface;
use Magento\Swatches\Helper\DataFactory;
use Magento\Swatches\Helper\Media;
use Magento\Swatches\Model\Swatch;
use function in_array;

/**
 * Class ProductAttributeHelper
 * @package FiloBlu\Refilo\Helper\Catalog
 */
class ProductAttributeHelper
{
    /** @var StoreInterface */
    protected $currentStore = null;
    /** @var array|null */
    protected $currentAttributes = null;
    /** @var array */
    protected $imagePlaceholderAttributes = [];
    /** @var array */
    protected $attributes;
    /** @var array */
    protected $filterableAttributes = [];
    /** @var array */
    protected $sortableAttributes = [];
    /** @var array */
    protected $lookupOptions = [];
    /** @var string[] */
    private $mandatoryAttributes = [];
    /** @var string[] */
    private $excludedAttributes = [];
    /** @var SearchCriteriaBuilder */
    private $searchCriteriaBuilder;
    /** @var SortOrderBuilder */
    private $sortOrderBuilder;
    /** @var AttributeRepositoryInterface */
    private $attributeRepository;
    /** @var StoreManagerInterface */
    private $storeManager;
    /** @var DataFactory */
    private $swatchHelperFactory;
    /** @var array */
    private $searchableAttributes;
    /** @var Media */
    private $swatchMediaHelper;
    /**
     * @var Collection
     */
    private $productCollection;


    /**
     * ProductAttributeHelper constructor.
     * @param SearchCriteriaBuilder $searchCriteriaBuilder
     * @param SortOrderBuilder $sortOrderBuilder
     * @param AttributeRepositoryInterface $attributeRepository
     * @param StoreManagerInterface $storeManager
     * @param DataFactory $swatchHelperFactory
     * @param Media $swatchMediaHelper
     * @param CollectionFactory $productCollection
     * @param array $mandatoryAttributes
     * @param array $excludedAttributes
     */
    public function __construct(
        SearchCriteriaBuilder        $searchCriteriaBuilder,
        SortOrderBuilder             $sortOrderBuilder,
        AttributeRepositoryInterface $attributeRepository,
        StoreManagerInterface        $storeManager,
        DataFactory                  $swatchHelperFactory,
        Media                        $swatchMediaHelper,
        CollectionFactory            $productCollection,
        array                        $mandatoryAttributes,
        array                        $excludedAttributes
    )
    {
        $this->searchCriteriaBuilder = $searchCriteriaBuilder;
        $this->sortOrderBuilder = $sortOrderBuilder;
        $this->attributeRepository = $attributeRepository;
        $this->storeManager = $storeManager;
        $this->swatchHelperFactory = $swatchHelperFactory;
        $this->swatchMediaHelper = $swatchMediaHelper;
        $this->productCollection = $productCollection;
        $this->excludedAttributes = $excludedAttributes;
        $this->mandatoryAttributes = $mandatoryAttributes;
    }

    /**
     * @param ProductInterface $product
     * @return array
     * @throws NoSuchEntityException
     */
    public function getAttributesValues(ProductInterface $product)
    {
        $superAttributes = $this->getExcludedAttributes();

        if ($product->getTypeId() === Configurable::TYPE_CODE) {
            foreach ($product->getTypeInstance()->getUsedProductAttributes($product) as $attribute) {
                $superAttributes[] = $attribute->getAttributeCode();
            }
        }
        return $this->getAttributesFromProduct($product, $this->getAttributes(), $superAttributes);
    }

    /**
     * @return array|string[]
     */
    public function getExcludedAttributes(): array
    {
        return $this->excludedAttributes;
    }

    /**
     *
     *
     * @param ProductInterface $product
     * @param array $attributeList
     * @param array $excludeList
     * @return array
     */
    protected function getAttributesFromProduct(
        ProductInterface $product,
        array            $attributeList,
        array            $excludeList = []
    ): array
    {
        $storeId = $product->getStore()->getId();

        $attributes = [];

        foreach ($attributeList[$storeId] as $code => $attribute) {
            if (in_array($code, $excludeList, true)) {
                continue;
            }

            $type = $attribute['type'];

            switch ($type) {
                case 'select':
                    $attributes[$code] = [
                        'title' => $attribute['title'],
                        'value' => $this->lookupOptions[$storeId][$code]['value'][$product->getData($code)] ?? null,
                        'key' => $product->getData($code)
                    ];

                    if ($attribute['isSwatch']) {
                        $data = $this->getSwatchByOptionId($product->getData($code));
                        $attributes[$code] = array_merge_recursive($attributes[$code], $data);
                    }

                    break;
                case 'multiselect':
                    $value = [];

                    $attributeData = explode(',', $product->getData($code) ?? '');

                    if (is_scalar($attributeData)) {
                        $attributeData = [$attributeData];
                    }

                    $keys = array_map('\intval', array_filter($attributeData, '\strlen'));

                    foreach ($keys as $k) {
                        if (isset($this->lookupOptions[$storeId][$code]['value'][$k])) {
                            $value[] = $this->lookupOptions[$storeId][$code]['value'][$k];
                        }
                    }

                    $attributes[$code] = [
                        'title' => $attribute['title'],
                        'value' => $value,
                        'key' => $keys
                    ];
                    break;
                default:
                    $attributes[$code] = [
                        'value' => $product->getData($code),
                        'title' => $attribute['title']
                    ];
                    break;
            }
        }

        return $attributes;
    }

    /**
     * @param $optionId
     * @return array
     */
    public function getSwatchByOptionId($optionId)
    {
        $output = [
            'swatch' => [
                'type' => 'empty',
                'value' => ''
            ]
        ];
        //Added factory cause helper cache data and sync wrong swatch
        $swatchHelper = $this->swatchHelperFactory->create();
        $swatches = $swatchHelper->getSwatchesByOptionsId([$optionId]);

        if (!isset($swatches[$optionId]['type'], $swatches[$optionId]['value'])) {
            return $output;
        }

        $type = (int)$swatches[$optionId]['type'];
        $value = $swatches[$optionId]['value'];

        switch ($type) {
            case Swatch::SWATCH_TYPE_VISUAL_IMAGE:
                // To ensure image presence and creation
                $this->swatchMediaHelper->getSwatchAttributeImage(Swatch::SWATCH_IMAGE_NAME, $value);

                $imagePath = implode(
                    '/',
                    [
                        '/attribute/swatch',
                        Swatch::SWATCH_IMAGE_NAME,
                        $this->swatchMediaHelper->getFolderNameSize(Swatch::SWATCH_IMAGE_NAME),
                        trim((string)$value, '/')
                    ]
                );

                // Backward compatibility
                return [
                    'type' => 'swatch',
                    'image' => $imagePath,
                    'swatch' => [
                        'type' => 'swatch',
                        'value' => $imagePath
                    ]
                ];
            case Swatch::SWATCH_TYPE_VISUAL_COLOR:
                return [
                    'swatch' => [
                        'type' => 'color',
                        'value' => $value
                    ]
                ];
            case Swatch::SWATCH_TYPE_TEXTUAL:
                return [
                    'swatch' => [
                        'type' => 'textual',
                        'value' => $value
                    ]
                ];
            case Swatch::SWATCH_TYPE_EMPTY:
            default:
                return $output;
        }
    }

    /**
     *
     * @throws NoSuchEntityException
     */
    public function getAttributes()
    {
        if ($this->attributes !== null) {
            return $this->attributes;
        }

        foreach ($this->storeManager->getStores() as $store) {
            $storeId = $store->getId();
            $this->filterableAttributes[$storeId] = [];
            $this->sortableAttributes[$storeId] = [];
            $this->attributes[$storeId] = [];
        }

        $currentStore = $this->storeManager->getStore();

        foreach ($this->storeManager->getStores(true) as $store) {
            $storeId = $store->getId();
            $this->storeManager->setCurrentStore($store);
            $sortOrder1 = $this->sortOrderBuilder->setField('position')->setDirection(SortOrder::SORT_ASC)->create();
            $sortOrder2 = $this->sortOrderBuilder->setField('attribute_code')->setDirection(
                SortOrder::SORT_ASC
            )->create();
            $searchCriteria = $this->searchCriteriaBuilder->addSortOrder($sortOrder1)->addSortOrder(
                $sortOrder2
            )->create();
            $response = $this->attributeRepository->getList('catalog_product', $searchCriteria);

            /** @var Attribute $attribute */
            foreach ($response->getItems() as $attribute) {
                // Force to reload attribute so labels are loaded in the right way
                $attribute->setStore($store)->load($attribute->getId());
                $attributeCode = $attribute->getAttributeCode();

                $attributeData = $this->getAttributeData(
                    $attribute,
                    $store
                );

                if (!empty($attributeData)) {
                    $this->attributes[$store->getId()][$attribute->getAttributeCode()] = $attributeData;
                    if ($attribute->getIsSearchable()) {
                        $this->searchableAttributes[$storeId][$attributeCode] = $attributeData;
                    }

                    if ($attribute->getIsFilterable()) {
                        $this->filterableAttributes[$storeId][$attributeCode] = $attributeData;
                    }

                    if ($attribute->getUsedForSortBy()) {
                        $this->sortableAttributes[$storeId][$attributeCode] = $attributeData;
                    }
                }

                if ($attribute->getFrontendInput() === 'media_image') {
                    $this->imagePlaceholderAttributes[$attribute->getAttributeCode()] = $attribute->getAttributeCode();
                }
            }
        }

        $this->storeManager->setCurrentStore($currentStore);

        return $this->attributes;
    }

    /**
     * @param Attribute $attribute
     * @param StoreInterface $store
     * @return array| null
     */
    public function getAttributeData(Attribute $attribute, StoreInterface $store): array
    {
        $attributeData = [];

        $attributeCode = $attribute->getAttributeCode();

        if (!$attribute->getIsVisibleOnFront() && !$attribute->getUsedInProductListing() && !$this->isMandatory(
                $attributeCode
            )) {
            return $attributeData;
        }

        $storeId = $store->getId();

        $swatchHelper = $this->swatchHelperFactory->create();
        $isSwatch = (bool)$swatchHelper->isSwatchAttribute($attribute);

        $attributeData = [
            'title' => trim($attribute->getStoreLabel($storeId) ?? ''),
            'code' => $attributeCode,
            'type' => $attribute->getFrontendInput(),
            'isSwatch' => $isSwatch,
            'isSearchable' => (bool)$attribute->getIsSearchable(),
            'isFilterable' => (bool)$attribute->getIsFilterable(),
            'isSortable' => (bool)$attribute->getUsedForSortBy(),
            'searchable' => $attribute->getIsSearchable(), // TODO: remove?
            'search_weight' => $attribute->getSearchWeight(),
            'sort_weight' => $attribute->getSortWeight($attribute->getAttributeSetId()),
            'position' => (int)$attribute->getPosition(),
            'options' => [],
            'value' => []
        ];

        if ($attribute->usesSource()) {
            foreach ($attribute->setStoreId($storeId)->getSource()->getAllOptions() as $option) {
                $this->lookupOptions[$storeId][$attributeCode]['value'][$option['value']] = (string)$option['label'];
                if (isset($attributeData['options'])) {
                    $optionValue = [
                        'label' => trim((string)$option['label']),
                        'key' => trim($option['value']),
                        'swatch' => []
                    ];

                    if ($isSwatch) {
                        $optionValue = array_merge_recursive(
                            $optionValue,
                            $this->getSwatchByOptionId($option['value'])
                        );
                    }

                    $attributeData['options'][] = $optionValue;
                }
            }
        }

        return $attributeData;
    }

    /**
     * @param string $attributeCode
     * @return bool
     */
    public function isMandatory(string $attributeCode): bool
    {
        return in_array($attributeCode, $this->getMandatoryAttributes(), false);
    }

    /**
     * @return array|string[]
     */
    public function getMandatoryAttributes(): array
    {
        return $this->mandatoryAttributes;
    }

    /**
     * @param ProductInterface $product
     * @param array $excludeList
     * @return array
     * @throws NoSuchEntityException
     */
    public function getFilterableAttributesWithChildren(ProductInterface $product, array $excludeList = []): array
    {
        $attributeList = $this->getFilterableAttributesList();
        $children = $product->getTypeInstance()->getChildrenIds($product->getId());
        $storeId = $product->getStore()->getId();
        $products = [$product->getId() => $product];

        if (!empty($children)) {
            //TODO get simple product from grouped
            $attributeCodes = array_keys($attributeList[$storeId]);
            $productCollection = $this->productCollection->create();
            $productCollection
                ->setStore($product->getStore())
                ->addIdFilter(current($children))
                ->addAttributeToSelect($attributeCodes);
            $products = $productCollection->getItems() + [$product->getId() => $product];
        }

        $superAttributes = $excludeList;

        if ($product->getTypeId() === Configurable::TYPE_CODE) {
            foreach ($product->getTypeInstance()->getUsedProductAttributes($product) as $attribute) {
                $superAttributes[] = $attribute->getAttributeCode();
            }
        }

        $reduced = array_reduce($products, function ($carry, $item) use ($storeId, $attributeList, $excludeList, $superAttributes) {

            if ($item->getStatus() != Status::STATUS_ENABLED || !$item->isSalable()) {
                return $carry;
            }

            if ($item->getTypeId() === Configurable::TYPE_CODE) {
                $excludeList = $superAttributes;
            }

            foreach ($attributeList[$storeId] as $code => $attribute) {
                if (in_array($code, $excludeList, true)) {
                    continue;
                }

                $type = $attribute['type'];
                $carry[$code]['title'] = $attribute['title'];
                $carry[$code]['type'] = $type;
                $productValue = $item->getResource()->getAttributeRawValue($item->getId(), $code, $item->getStoreId());

                if (empty($productValue)) {
                    continue;
                }

                switch ($type) {
                    case 'select':
                        if (isset($this->lookupOptions[$storeId][$code]['value'][$productValue])) {
                            $carry[$code]['values'][$productValue] = $this->lookupOptions[$storeId][$code]['value'][$productValue] ?? null;
                        }

                        break;
                    case 'multiselect':
                        $attributeData = explode(',', $productValue ?? '');

                        if (is_scalar($attributeData)) {
                            $attributeData = [$attributeData];
                        }

                        $keys = array_map('\intval', array_filter($attributeData, '\strlen'));

                        foreach ($keys as $k) {
                            if (isset($this->lookupOptions[$storeId][$code]['value'][$k])) {
                                $carry[$code]['values'][$k] = $this->lookupOptions[$storeId][$code]['value'][$k];
                            }
                        }

                        break;
                    default:
                        $carry[$code]['values'][$productValue] = $productValue;
                        break;
                }
            }

            return $carry;
        }, []);

        $attributes = [];
        foreach ($reduced as $code => $data) {
            if (!isset($data['values'])) {
                continue;
            }
            $attributes[$code] = [
                'title' => $data['title'],
                'key' => array_keys($data['values']),
                'value' => array_values($data['values'])
            ];
        }
        return $attributes;
    }

    /**
     * @return array
     * @throws NoSuchEntityException
     */
    public function getFilterableAttributesList()
    {
        $this->getAttributes();
        return $this->filterableAttributes;
    }

    /**
     * @param ProductInterface $product
     * @return array
     * @throws NoSuchEntityException
     */
    public function getImagePlaceholdersAttributes(ProductInterface $product): array
    {
        $this->getAttributes();

        $placeholders = [];

        foreach ($this->imagePlaceholderAttributes as $attributeCode) {
            $value = $product->getData($attributeCode);

            if ($value === null) {
                continue;
            }

            // https://filoblu.atlassian.net/browse/BARR-1405
            if ($value === 'no_selection') {
                continue;
            }

            $placeholders[$attributeCode] = "/catalog/product$value";
        }

        return $placeholders;
    }

    /**
     * @param $optionId
     * @param $storeId
     * @param $attributeCode
     * @return mixed|null
     * @throws NoSuchEntityException
     */
    public function getProductOptionLabel($optionId, $storeId, $attributeCode)
    {
        $attributes = $this->getAttributes();

        if (!isset($attributes[$storeId][$attributeCode]['options'])) {
            return null;
        }

        foreach ($attributes[$storeId][$attributeCode]['options'] as $option) {
            if ($option['key'] == $optionId) {
                return $option['label'];
            }
        }
        return null;
    }

    /**
     * @param $attributeCode
     * @param $id
     * @return mixed|null
     * @throws NoSuchEntityException
     */
    public function getAdminValue($attributeCode, $id)
    {
        $this->getAttributes();
        return $this->lookupOptions[0][$attributeCode]['value'][$id] ?? null;
    }

    /**
     * @return array
     */
    public function getLookupOptions()
    {
        return $this->lookupOptions;
    }
    /**
     * @param string $value
     * @return string
     */
    public function checkHtmlAttribute(string $value): string
    {
        if ($value === '' || (strip_tags($value) === '')) {
            return '';
        }

        return $value;
    }

    /**
     * @return Data
     */
    public function getSwatchHelper(): Data
    {
        return $this->swatchHelperFactory->create();
    }

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

    /**
     * @return AttributeRepositoryInterface
     */
    public function getAttributeRepository(): AttributeRepositoryInterface
    {
        return $this->attributeRepository;
    }

    /**
     * @throws NoSuchEntityException
     */
    public function getFilterableAttributesCode(ProductInterface $product): array
    {
        return array_keys($this->getFilterableAttributes($product)) ?? [];
    }

    /**
     * @param ProductInterface $product
     * @return array
     * @throws NoSuchEntityException
     */
    public function getFilterableAttributes(ProductInterface $product): array
    {
        return $this->getAttributesFromProduct($product, $this->getFilterableAttributesList());
    }

    /**
     * @throws NoSuchEntityException
     */
    public function getSearchableAttributesCode(ProductInterface $product): array
    {
        return array_keys($this->getSearchableAttributes($product)) ?? [];
    }

    /**
     * @param ProductInterface $product
     * @return array
     * @throws NoSuchEntityException
     */
    public function getSearchableAttributes(ProductInterface $product)
    {
        $this->getAttributes();
        return $this->getAttributesFromProduct($product, $this->searchableAttributes);
    }

    /**
     * @throws NoSuchEntityException
     */
    public function getSortableAttributesCode(ProductInterface $product): array
    {
        return array_keys($this->getSortableAttributes($product)) ?? [];
    }

    /**
     * @param ProductInterface $product
     * @return array
     * @throws NoSuchEntityException
     */
    public function getSortableAttributes(ProductInterface $product)
    {
        $this->getAttributes();
        return $this->getAttributesFromProduct($product, $this->sortableAttributes);
    }
}
