<?php

namespace FiloBlu\Flow\Model\Actions\From;

use Exception;
use FiloBlu\Flow\Model\System\ProductAttributes;
use Magento\Catalog\Model\Product\Attribute\Source\Status;
use Magento\Catalog\Model\Product\Type;
use Magento\Catalog\Model\Product\Visibility;
use Magento\Catalog\Model\ProductFactory;
use Magento\Catalog\Model\ResourceModel\Product;
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
use Magento\ConfigurableProduct\Model\Product\Type\Configurable\AttributeFactory as ConfigurableAttributeFactory;
use Magento\ConfigurableProduct\Model\Product\Type\ConfigurableFactory;
use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable as ConfigurableResourceModel;
use Magento\Eav\Model\Entity\AttributeFactory as EavAttributeFactory;
use Magento\Eav\Model\ResourceModel\Entity\AttributeFactory;
use Magento\Store\Model\StoreManagerInterface;
use RuntimeException;
use function in_array;
use function is_string;
use function sprintf;

/**
 * Class Addtoconfigurable
 * @package FiloBlu\Flow\Model\Actions\From
 */
class Addtoconfigurable
{
    /**
     * @var string
     */
    const ENTITY_TYPE_CODE = 'catalog_product';

    /**
     * @var ProductFactory
     */
    protected $productFactory;

    /**
     * @var
     */
    protected $passedVisibility;

    /**
     * @var \FiloBlu\Flow\Helper\Product
     */
    protected $helperProduct;

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

    /**
     * @var AttributeFactory
     */
    protected $attributeFactory;

    /**
     * @var Product
     */
    protected $productResource;

    /**
     * @var ConfigurableFactory
     */
    protected $typeConfigurableFactory;

    /**
     * @var Configurable\AttributeFactory
     */
    protected $configurableAttributeFactory;

    /**
     * @var ConfigurableResourceModel
     */
    protected $configurableResourceModel;

    /**
     * @var EavAttributeFactory
     */
    protected $eavAttributeFactory;

    /**
     * @var ProductAttributes
     */
    protected $productAttributesHelper;

    /**
     * @var string
     */
    private $separator = '#';

    /**
     * Addtoconfigurable constructor.
     * @param \FiloBlu\Flow\Helper\Product $helperProduct
     * @param ProductFactory $productFactory
     * @param StoreManagerInterface $storeManager
     * @param AttributeFactory $attributeFactory
     * @param Product $productResource
     * @param ConfigurableFactory $typeConfigurableFactory
     * @param ConfigurableAttributeFactory $configurableAttributeFactory
     * @param ConfigurableResourceModel $configurableResourceModel
     * @param EavAttributeFactory $eavAttributeFactory
     * @param ProductAttributes $productAttributesHelper
     */
    public function __construct(
        \FiloBlu\Flow\Helper\Product $helperProduct,
        ProductFactory               $productFactory,
        StoreManagerInterface        $storeManager,
        AttributeFactory             $attributeFactory,
        Product                      $productResource,
        ConfigurableFactory          $typeConfigurableFactory,
        ConfigurableAttributeFactory $configurableAttributeFactory,
        ConfigurableResourceModel    $configurableResourceModel,
        EavAttributeFactory          $eavAttributeFactory,
        ProductAttributes            $productAttributesHelper
    )
    {
        $this->helperProduct = $helperProduct;
        $this->productFactory = $productFactory;
        $this->storeManager = $storeManager;
        $this->attributeFactory = $attributeFactory;
        $this->productResource = $productResource;
        $this->typeConfigurableFactory = $typeConfigurableFactory;
        $this->configurableAttributeFactory = $configurableAttributeFactory;
        $this->configurableResourceModel = $configurableResourceModel;
        $this->eavAttributeFactory = $eavAttributeFactory;
        $this->productAttributesHelper = $productAttributesHelper;
    }

    /**
     * @param $simpleProduct
     * @param $value
     * @param $storeId
     * @param null $visibility
     * @throws Exception
     */
    public function process($simpleProduct, $value, $storeId, $visibility = null)
    {
        $this->passedVisibility = $visibility;

        if (empty($value)) {
            throw new RuntimeException("Invalid value supplied '{$value}'");
        }

        if (!$simpleProduct->getId()) {
            throw new RuntimeException('Addtoconfigurable: Simple product is invalid');
        }

        if ($storeId !== 0) {
            throw new RuntimeException('Addtoconfigurable: The function is avalaible only for the ADMIN store.');
        }

        $parentField = null;
        $this->storeManager->setCurrentStore($storeId);

        $pieces = explode($this->getSeparator(), (string)$value);

        if($pieces === false || count($pieces) < 2) {
            throw new RuntimeException('Addtoconfigurable: Invalid value supplied');
        }

        $pieces = array_map('trim', $pieces);
        $configurableSku = $pieces[0];
        $superAttributeIds = explode(',', $pieces[1]);
        if (count($pieces) > 2) {
            $parentField = $pieces[2];
        }
        unset($pieces);

        if ($configurableSku === $simpleProduct->getSku()) {
            throw new RuntimeException('Addtoconfigurable: Configurable SKU cannot be equal to Simple SKU');
        }

        //check if the simple product has a value for all the super attributes...
        $eavAttributeModel = $this->attributeFactory->create();
        $superValuesMap = [];

        foreach ($superAttributeIds as $attributeCode) {
            $attributeId = is_numeric($attributeCode) ? $attributeCode : $eavAttributeModel->getIdByCode('catalog_product', $attributeCode);
            $value = $this->productResource->getAttributeRawValue($simpleProduct->getId(), $attributeId, $storeId);
            // Values with 0 integer do not need to be blocked
            if (trim($value) === '') {
                $message = sprintf('Simple product #%s has no value for super attribute \'%s\'. All super attributes must have a value in the simple product.', $simpleProduct->getId(), $attributeId);
                throw new RuntimeException($message);
            }

            $superValuesMap[$attributeId] = $value;
        }

        // Getting the configurable product...
        $productId = $this->productFactory->create()->getIdBySku($configurableSku);
        $configurableProduct = $this->productFactory->create()->setStoreId($storeId)->load($productId);

        if ($configurableProduct->getId()) {

            if ($configurableProduct->getTypeId() !== Configurable::TYPE_CODE) {
                throw new RuntimeException(
                    sprintf('Product with sku %s already exists as \'%s\' instead of \'%s\'',
                        $configurableSku, $configurableProduct->getTypeId(), Configurable::TYPE_CODE)
                );
            }

            $this->_syncAttributesFromSimpleToConfigurable($simpleProduct->getId(), $configurableProduct, $configurableProduct->getStoreIds());
        } else {
            $configurableProduct = $this->_createConfigurableFromSimple($configurableSku, $superAttributeIds, $simpleProduct, $storeId);
            if (is_string($configurableProduct)) {
                return;
            }
        }

        // Getting current associated children...

        $childIds = $this->typeConfigurableFactory->create()->getChildrenIds($configurableProduct->getId());
        $childIds = $childIds[0];

        // Check if this simple product is already associated...

        if (in_array($simpleProduct->getId(), $childIds, false)) {
            //resetting visibility and status...
            $this->resettingVisibilityAndStatus($parentField, $configurableSku, $simpleProduct, $storeId);

            return;
        }

        // Check if already exists a product with the same super attributes values associated to the configurable product...
        $sameValuesProduct = $configurableProduct->getTypeInstance()->getProductByAttributes($superValuesMap, $configurableProduct);
        if ($sameValuesProduct !== null) {
            $message = sprintf('Product #%s with same super attributes values of simple #%s is already associated to the configurable product.',"{$sameValuesProduct->getId()} ({$sameValuesProduct->getSku()})","{$simpleProduct->getId()} ({$simpleProduct->getSku()})");
            throw new RuntimeException($message);
        }

        //adding simple to configurable...
        $childIds[] = $simpleProduct->getId();
        $this->configurableResourceModel->saveProducts($configurableProduct, $childIds);

        /* needed for correct index prices calculations: START */
        $shouldSaveConfigurable = false;
        if ($configurableProduct->getData('has_options') === 0) {
            $configurableProduct->setData('has_options', 1);
            $shouldSaveConfigurable = true;
        }

        if ($configurableProduct->getData('required_options') === 0) {
            $configurableProduct->setData('required_options', 1);
            $shouldSaveConfigurable = true;
        }

        if ($shouldSaveConfigurable) {
            $configurableProduct->setAssociatedProductIds($childIds);
            $configurableProduct->save();
        }
        /* needed for correct index prices calculations: END */

        //hide the simple product visibility...
        $this->resettingVisibilityAndStatus($parentField, $configurableSku, $simpleProduct, $storeId);
    }

    /**
     * @return string
     */
    public function getSeparator()
    {
        return $this->separator;
    }

    /**
     * @param $separator
     * @return $this
     */
    public function setSeparator($separator)
    {
        $this->separator = $separator;
        return $this;
    }

    /**
     * @param $configurableSku
     * @param $superAttributeIds
     * @param $simpleProduct
     * @param int $storeId
     * @return \Magento\Catalog\Model\Product|string
     * @throws Exception
     */
    protected function _createConfigurableFromSimple($configurableSku, $superAttributeIds, $simpleProduct, $storeId = 0)
    {
        $configurableProduct = $this->productFactory->create();
        $configurableProduct->setTypeId(Configurable::TYPE_CODE);
        $configurableProduct->setStoreId($storeId);

        //setting the super attributes to the configurable product...
        $superAttributes = [];
        $eavAttributeModel = $this->attributeFactory->create();
        foreach ($superAttributeIds as $superAttributeId) {
            $attributeId = is_numeric($superAttributeId) ? $superAttributeId : $eavAttributeModel->getIdByCode('catalog_product', $superAttributeId);
            $attribute = $this->eavAttributeFactory->create();
            $attribute->load($attributeId);
            if (!$attribute->getId()) {
                return sprintf('Super attribute with id #%d not found.', $superAttributeId);
            }

            $configurableAttribute = $this->configurableAttributeFactory->create()->setProductAttribute($attribute);

            $superAttributes[] = [
                'id'             => $configurableAttribute->getId(),
                'label'          => $configurableAttribute->getLabel(),
                'position'       => $attribute->getPosition(),
                'values'         => $configurableAttribute->getPrices() ?: [],
                'attribute_id'   => $attribute->getId(),
                'attribute_code' => $attribute->getAttributeCode(),
                'frontend_label' => $attribute->getFrontend()->getLabel(),
                'use_default'    => 1
            ];
        }

        $configurableProduct->setCanSaveConfigurableAttributes(true);
        $configurableProduct->setConfigurableAttributesData($superAttributes);

        //copy all the attributes of specified groups from the simple product...
        $attributesToCopy = $this->productAttributesHelper->getAttributesToCopy()->getColumnValues('attribute_code');
        $excludedAttributes = array_diff($attributesToCopy, $this->helperProduct->getSimpleToConfigurableSyncAttributes());
        $attributesToCopy = array_diff($attributesToCopy, $excludedAttributes);
        foreach ($attributesToCopy as $attributeCode) {
            $configurableProduct->setData($attributeCode, $simpleProduct->getData($attributeCode));
            if ($simpleProduct->hasData($attributeCode)) {
                $configurableProduct->setData($attributeCode, $simpleProduct->getData($attributeCode));
            }
        }

        //setting new attributes...
        if (!$configurableProduct->getName()) {
            $configurableProduct->setName($simpleProduct->getName());
        }

        if (!$configurableProduct->getDescription()) {
            $configurableProduct->setDescription($simpleProduct->getDescription());
        }

        if (!$configurableProduct->getShortDescription()) {
            $configurableProduct->setShortDescription($simpleProduct->getShortDescription());
        }

        if (!$configurableProduct->getWeight()) {
            $configurableProduct->setWeight(0);
        }


        // If i have the visibility forced, then I will force it
        // else fallback to VISIBILITY_BOTH
        if ($this->passedVisibility) {
            $visibility = $this->passedVisibility;
        } else {
            $visibility = Visibility::VISIBILITY_BOTH;
        }

        $configurableProduct->setSku($configurableSku);
        $configurableProduct->setStatus(Status::STATUS_DISABLED);
        $configurableProduct->setVisibility(Visibility::VISIBILITY_BOTH);
        $configurableProduct->setAttributeSetId($simpleProduct->getAttributeSetId());
        $configurableProduct->setTaxClassId($simpleProduct->getTaxClassId());
        $configurableProduct->setPrice(0);
        $configurableProduct->setPriceView(0);
        $configurableProduct->setCategoryIds($simpleProduct->getCategoryIds());
        $configurableProduct->setWebsiteIds($simpleProduct->getWebsiteIds());
        $configurableProduct->setStockData(['is_in_stock' => 1]);
        /* needed for correct index prices calculations: START */
        $configurableProduct->setData('has_options', 1);
        $configurableProduct->setData('required_options', 1);
        $configurableProduct->setUseDefault(1);

        /* needed for correct index prices calculations: END */
        try {
            $configurableProduct->save();
        } catch (Exception $e) {
            if ($e->getMessage() === 'URL key for specified store already exists.') {
                $urlKey = $configurableProduct->getUrlKey();
                $newUrlKey = $urlKey . '-' . $configurableProduct->getId();
                $configurableProduct->setUrlKey($newUrlKey);
                $configurableProduct->save();
            }
        }

        //copy localized attributes from the other stores...
        $this->_syncAttributesFromSimpleToConfigurable($simpleProduct->getId(), $configurableProduct, $configurableProduct->getStoreIds());
        return $configurableProduct;
    }

    /**
     * @param $simpleId
     * @param $configurableProduct
     * @param $storeIds
     * @return bool
     */
    protected function _syncAttributesFromSimpleToConfigurable($simpleId, $configurableProduct, $storeIds)
    {
        $attributesToCopy = $this->helperProduct->getSimpleToConfigurableSyncAttributes();

        if (empty($attributesToCopy)) {
            return false;
        }

        $superAttributesCode = [];
        // se il configurabile perde i prodotti semplici associati, torna ad essere un semplice, e a riga 275 darà errore - va gestito il caso
        if ($configurableProduct->getTypeId() !== Type::DEFAULT_TYPE) {
            foreach ($configurableProduct->getTypeInstance()->getConfigurableAttributesAsArray($configurableProduct) as $superAttribute) {
                $superAttributesCode[] = $superAttribute['attribute_code'];
            }
        }

        $attributesToCopy = array_diff($attributesToCopy, $superAttributesCode);
        if (empty($attributesToCopy)) {
            return false;
        }

        array_unshift($storeIds, '0');

        foreach ($storeIds as $storeId) {

            $simple = $this->productFactory->create()->setStoreId($storeId)->load($simpleId);
            $attributesData = [];
            foreach ($attributesToCopy as $attributeCode) {
                // need to copy only the values actually present for the stores, excluding "use default"
                if ($attributeCode === 'url_key' && $this->helperProduct->getUpdateConfigurableUrl()) {
                    $name = $simple->getData('name');
                    $confUrlkey = $configurableProduct->formatUrlKey($name . '-' . $storeId . $configurableProduct->getId());
                    $attributesData[$attributeCode] = $confUrlkey;
                } else {
                    $attributesData[$attributeCode] = $simple->getData($attributeCode);
                }
            }

            foreach($attributesData as $attrCode => $attrValue) {
                $configurableProduct->addAttributeUpdate($attrCode, $attrValue, $storeId);
            }
            unset($simple, $attributesData);
        }

        return true;
    }

    /**
     * @param string $parentField
     * @param string $configurableSku
     * @param $simpleProduct
     * @param int $storeId
     * @return void
     */
    public function resettingVisibilityAndStatus(string $parentField, string $configurableSku, $simpleProduct, int $storeId)
    {
        $attributesData = [
            'status' => Status::STATUS_ENABLED,
            'visibility' => Visibility::VISIBILITY_NOT_VISIBLE
        ];
        // If I have the visibility forced, then I will force it
        if ($this->passedVisibility) {
            $attributesData['visibility'] = $this->passedVisibility;
        }

        if (!empty($parentField)) {
            $attributesData[$parentField] = $configurableSku;
        }

        foreach ($attributesData as $attrCode => $attrValue) {
            $simpleProduct->addAttributeUpdate($attrCode, $attrValue, $storeId);
        }
    }

    /**
     * @param $productId
     */
    protected function _prepareProductForSpecialPrice($productId)
    {
        if ($this->productResource->getAttributeRawValue($productId, 'special_price', .0)) {
            $product = $this->productFactory->create()->load($productId);
            $product->addAttributeUpdate('special_price', null, 0);
        }
    }

    /**
     * @param $configurableId
     * @param $storeId
     * @param null $websiteId
     */
    protected function _setConfigurablePrice($configurableId, $storeId, $websiteId = null)
    {

    }

}

