<?php

namespace FiloBlu\Refilo\Remote\Entity;

use Exception;
use FiloBlu\Refilo\Helper\Catalog\ProductAttributeHelper;
use FiloBlu\Refilo\Helper\Catalog\ProductHelper;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Model\Product\Attribute\Source\Status;
use Magento\Catalog\Model\Product\Attribute\Source\Status as AttributeStatus;
use Magento\Catalog\Model\Product\Option;
use Magento\Catalog\Model\Product\Type;
use Magento\ConfigurableProduct\Api\Data\OptionInterface;
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\Serialize\SerializerInterface;
use Magento\GroupedProduct\Model\Product\Type\Grouped;

use Psr\Log\LoggerInterface;

use Throwable;

use function array_key_exists;
use function is_array;

/**
 * Class MagentoToRefiloProductAdapter
 * @package FiloBlu\Refilo\Remote\Entity
 */
class MagentoToRefiloProductAdapter
{
    /**
     * @var ProductHelper
     */
    private $productHelper;

    /**
     * @var Option
     */
    private $option;

    /**
     * @var ProductAttributeHelper
     */
    private $productAttributeHelper;

    /**
     * @var \Psr\Log\LoggerInterface
     */
    private $logger;
    /**
     * @var \Magento\Framework\Serialize\SerializerInterface
     */
    private $serializer;

    /**
     * MagentoToRefiloProductAdapter constructor.
     * @param ProductHelper $productHelper
     * @param Option $option
     * @param \Magento\Framework\Serialize\SerializerInterface $serializer
     * @param \Psr\Log\LoggerInterface $logger
     */
    public function __construct(
        ProductHelper $productHelper,
        Option $option,
        SerializerInterface $serializer,
        LoggerInterface  $logger
    ) {
        $this->productHelper = $productHelper;
        $this->productAttributeHelper = $productHelper->getProductAttributeHelper();
        $this->option = $option;
        $this->logger = $logger;
        $this->serializer = $serializer;
    }

    /**
     * @param \Magento\Catalog\Model\Product|ProductInterface $magentoProduct
     * @param Product $refiloProduct
     * @return Product
     * @throws NoSuchEntityException
     * @throws Exception
     */
    public function adapt($magentoProduct, Product $refiloProduct): Product
    {
        $docType = Product::DOCTYPE_SPEC_DISABLED;
        $visibility = '0';
        $searchVisibility = '0';

        if ((int)$magentoProduct->getStatus() === AttributeStatus::STATUS_ENABLED) {
            $docType = Product::DOCTYPE_SPEC_ENABLED;
            $visibility = (!$this->productHelper->isProductVisible($magentoProduct) ? '0' : '1');
            $searchVisibility = (!$this->productHelper->isProductVisibleInSearch($magentoProduct) ? '0' : '1');
        }

        $data = [
            Product::DOCTYPE           => $docType,
            AbstractEntity::ID         => (int)$magentoProduct->getEntityId(),
            Product::ENABLED           => $this->productHelper->isEnabled($magentoProduct),
            Product::SKU               => $magentoProduct->getSku(),
            Product::TYPE              => $magentoProduct->getTypeId(),
            Product::NAME              => $magentoProduct->getName(),
            Product::CREATED_AT        => $magentoProduct->getCreatedAt(),
            Product::UPDATED_AT        => $magentoProduct->getUpdatedAt(),
            Product::WEIGHT            => $magentoProduct->getWeight(),
            Product::META_TITLE        => $magentoProduct->getMetaTitle(),
            Product::META_KEYWORDS     => $magentoProduct->getMetaKeywords(),
            Product::META_DESCRIPTION  => $magentoProduct->getMetaDescription(),
            Product::VISIBLE           => $visibility,
            Product::IS_SALABLE        => $this->productHelper->getIsSalable($magentoProduct),
            Product::HIDE_PRICE        => $this->productHelper->getHidePrice($magentoProduct),
            Product::MIDDLE_PRICE      => (float)$magentoProduct->getMiddlePrice(),
            Product::OLD_PRICE         => (float)$magentoProduct->getOldPrice(),
            Product::ATTRIBUTES        => $this->productAttributeHelper->getAttributesValues($magentoProduct),
            Product::FILTERS           => $this->productAttributeHelper->getFilterableAttributesWithChildren($magentoProduct),
            Product::SORTABLES         => $this->productAttributeHelper->getSortableAttributes($magentoProduct),
            Product::UPSELL            => $magentoProduct->getUpSellProductIds(),
            Product::CROSSSELL         => $magentoProduct->getCrossSellProductIds(),
            Product::RELATED           => $magentoProduct->getRelatedProductIds(),
            Product::ASSETS            => $this->productHelper->getAssets($magentoProduct),
            Product::INVENTORY         => $this->productHelper->getStockFor($magentoProduct),
            Product::SEARCH_VISIBILITY => $searchVisibility,
            Product::CUSTOM_OPTIONS    => $this->getCustomOptions($magentoProduct),
            Product::CATEGORIES        => []
        ];

        $refiloProduct->setData(
            array_merge(
                $data,
                $this->productHelper->getPricesFor($magentoProduct),
                $this->productHelper->getCategoriesFor($magentoProduct),
                $this->getQuantityInformation($magentoProduct)
            )
        );

        return $this->fillChildren($magentoProduct, $refiloProduct);
    }

    /**
     * @param \Magento\Catalog\Model\Product $magentoProduct
     * @return array
     */
    public function getCustomOptions(\Magento\Catalog\Model\Product $magentoProduct): array
    {
        $customOptions = [];

        // TODO: memory leaked
        //
        // Do not call following collection because memory leak
        // $collection = $magentoProduct->getProductOptionsCollection();

        /** @var \Magento\Catalog\Model\ResourceModel\Product\Option $item */
        foreach ($this->option->getProductOptionCollection($magentoProduct)->getItems() as $item) {
            $data = $item->getData();
            $data['values'] = [];

            foreach ($item->getValues() ?? [] as $value) {
                $data['values'][] = $value->getData();
            }

            $customOptions[] = $data;
        }
        return $customOptions;
    }

    /**
     * @param \Magento\Catalog\Model\Product $magentoProduct
     * @param Product $refiloProduct
     * @return Product
     * @throws Exception
     */
    public function fillChildren(\Magento\Catalog\Model\Product $magentoProduct, Product $refiloProduct): Product
    {
        switch ($magentoProduct->getTypeId()) {
            case Configurable::TYPE_CODE:
                return $this->getConfigurableChildren($magentoProduct, $refiloProduct);
            case Type::TYPE_BUNDLE:
                return $this->getBundleChildren($magentoProduct, $refiloProduct);
            case Grouped::TYPE_CODE:
                return $this->getGroupedChildren($magentoProduct, $refiloProduct);
        }
        return $refiloProduct;
    }

    /**
     * @param OptionInterface $option
     * @param $usableAttributes
     * @param $storeId
     * @param $attributeCode
     * @return array
     * @throws \Magento\Framework\Exception\NoSuchEntityException
     */
    public function getOptions(OptionInterface $option, $usableAttributes, $storeId, $attributeCode)
    {
        $output = [];

        foreach ($option->getOptions() ?? [] as $optionValue) {
            if (!array_key_exists($optionValue['value_index'], $usableAttributes)) {
                continue;
            }
            $data = $this->productAttributeHelper->getSwatchByOptionId($optionValue['value_index']);
            $data['code'] = $this->productAttributeHelper->getAdminValue($attributeCode, $optionValue['value_index']);
            $data['value_index'] = $optionValue['value_index'];
            $data['label'] = $this->productAttributeHelper->getProductOptionLabel(
                $optionValue['value_index'],
                $storeId,
                $attributeCode
            );
            $output[] = $data;
        }

        return $output;
    }

    /**
     * @param ProductInterface $magentoProduct
     * @param Product $refiloProduct
     * @return Product
     */
    public function getBundleChildren(ProductInterface $magentoProduct, Product $refiloProduct)
    {
        $extensionAttributes = $magentoProduct->getExtensionAttributes();
        $options = [];

        $refiloProduct->setData('bundle_price_type', (int)$magentoProduct->getPriceType() == 0 ? 'dynamic' : 'fixed');
        foreach ($extensionAttributes->getBundleProductOptions() as $bundleProductOption) {
            $productLinks = $bundleProductOption->getData('product_links');

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

            $option = [
                'id'       => (int)$bundleProductOption->getData('option_id'),
                'title'    => $bundleProductOption->getData('title'),
                'required' => (int)$bundleProductOption->getData('required'),
                'type'     => $bundleProductOption->getData('type'),
                'position' => (int)$bundleProductOption->getData('position'),
                'childs'   => []
            ];

            foreach ($productLinks as $productLink) {
                $link_data = $productLink->getData();
                $link_data['product_id'] = $link_data['entity_id'];
                $link_data['product_sku'] = $link_data['sku'];

                unset($link_data['entity_id'], $link_data['sku'], $link_data['option_id']);

                $child = [];
                foreach ($link_data as $key => $value) {
                    $child[$key] = (is_numeric($value)) ? $value + 0 : $value;
                }

                $option['childs'][] = $child;
            }

            $options[] = $option;
        }

        $refiloProduct->setBundle($options);
        return $refiloProduct;
    }

    /**
     * @param ProductInterface|\Magento\Catalog\Model\Product $magentoProduct
     * @param Product $refiloProduct
     * @return Product
     */
    public function getGroupedChildren($magentoProduct, Product $refiloProduct): Product
    {
        /** @var Grouped $grouped */
        $grouped = $magentoProduct->getTypeInstance();
        $links = $grouped->getAssociatedProductIds($magentoProduct) ?? [];
        $refiloProduct->setChildren($links);
        return $refiloProduct;
    }

    /**
     * @param \Magento\Catalog\Model\Product|ProductInterface $magentoProduct
     */
    public function getQuantityInformation($magentoProduct): array
    {
        try {
            $stockInformation = $this->productHelper->getStockConfiguration($magentoProduct);

            return [
                Product::MIN_QTY => (int)$stockInformation->getMinSaleQty(),
                Product::MAX_QTY => (int)$stockInformation->getMaxSaleQty(),
                Product::STEP    => (int)$stockInformation->getQtyIncrements() ?: 1
            ];
        } catch (Throwable $throwable) {
            $this->logger->critical("Stock configuration for product {$magentoProduct->getSku()}", [
                'exception' => $throwable
            ]);
        }

        return [
            Product::MIN_QTY => 1,
            Product::MAX_QTY => 9999,
            Product::STEP    => 1
        ];
    }

    /**
     * @param \Magento\Catalog\Model\Product $magentoProduct
     * @param Product $refiloProduct
     * @return Product
     * @throws Exception
     */
    public function getConfigurableChildren(
        \Magento\Catalog\Model\Product $magentoProduct,
        Product $refiloProduct
    ): Product {

        if ($magentoProduct->getTypeId() !== Configurable::TYPE_CODE) {
            return $refiloProduct;
        }

        $childrenIds = [];
        $sellableAttributes = [];
        $storeId = $magentoProduct->getStore()->getId();
        $extensionAttributes = $magentoProduct->getExtensionAttributes();

        // TODO: memory leaked
        //
        // $configurable = $magentoProduct->getTypeInstance();
        // $usedAttributes = $configurable->getUsedProductAttributes($magentoProduct);
        //
        $usedAttributes = $this->productHelper->getUsedProductAttributes($magentoProduct);

        // TODO: memory leaked
        //
        // $configurable = $magentoProduct->getTypeInstance();
        // $productsChildren = $configurable->getUsedProducts($magentoProduct);
        //
        $filterableAttributes = $this->productAttributeHelper->getFilterableAttributesCode($magentoProduct);

        $productsChildren = $this->productHelper->getUsedProducts($magentoProduct, $usedAttributes, $filterableAttributes);

        foreach ($usedAttributes as $attribute) {
            $sellableAttributes[$attribute['attribute_code']] = [];
        }

        foreach ($productsChildren as $productChild) {
            /** @var ProductInterface $productChild */
            if ((int)$productChild->getStatus() !== AttributeStatus::STATUS_ENABLED) {
                continue;
            }

            foreach ($sellableAttributes as $attributeCode => $data) {
                $sellableAttributes[$attributeCode][$productChild->getData($attributeCode)] = true;
            }

            $childrenIds[] = (int)$productChild->getId();
        }

        $refiloProduct->setChildren($childrenIds);
        $productOptions = $extensionAttributes->getConfigurableProductOptions() ?? [];

        $variants = [];
        foreach ($productOptions as $option) {
            $attributeCode = $option->getProductAttribute()->getAttributeCode();
            $variant = [
                'id'             => $option->getId(),
                'type'           => $option->getProductAttribute()->getData('frontend_input'),
                'position'       => $option->getPosition(),
                'label'          => $option->getProductAttribute()->getStoreLabel($storeId),
                'use_default'    => (int)$option->getIsUseDefault(),
                'attribute_id'   => $option->getAttributeId(),
                'attribute_code' => $attributeCode,
                'values'         => $this->getOptions(
                    $option,
                    $sellableAttributes[$attributeCode],
                    $storeId,
                    $attributeCode
                )
            ];

            $additionalData = [];

            try {
                $additionalData = (array)$this->getSerializer()
                    ->unserialize((string)$option->getProductAttribute()->getData('additional_data'));
            } catch (\InvalidArgumentException $exception) {
            }

            if ($additionalData) {
                if (!$option->getProductAttribute()->getData('frontend_input') === 'select') {
                    $variant['rendered'] = '';
                    $variant['update_product_media'] = '';
                } else {
                    $variant['rendered'] = $additionalData['swatch_input_type'] ?? 'select';
                    $variant['update_product_media'] = $additionalData['update_product_preview_image'] ?? '0';
                }
            }

            $options = [];
            foreach ($variant['values'] as $value) {
                $options[] = (int)$value['value_index'];
            }

            $variant['options'] = $options;
            $variants[] = $variant;
        }

        $refiloProduct->setVariants($variants);
        $productOptions = null;
        return $refiloProduct;
    }

    /**
     * @return \Magento\Framework\Serialize\SerializerInterface
     */
    public function getSerializer(): SerializerInterface
    {
        return $this->serializer;
    }
}
