<?php

namespace FiloBlu\Flow\Model\From\Entity;

use Exception;
use FiloBlu\Flow\Helper\LoggerProvider;
use Magento\Catalog\Model\ProductFactory;
use Magento\Eav\Model\Entity\Attribute\Source\BooleanFactory;
use Magento\Eav\Model\Entity\AttributeFactory;
use Magento\Eav\Model\Entity\TypeFactory;
use Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\CollectionFactory;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\ObjectManagerInterface;
use Magento\Framework\Registry;
use Monolog\Logger;
use RuntimeException;
use function in_array;
use function is_array;

/**
 * Class AbstractEntity
 * @package FiloBlu\Flow\Model\From\Entity
 */
abstract class AbstractEntity
{
    /**
     * @var mixed|null
     */
    protected $entityTypeCode;
    /**
     * @var null
     */
    protected $entityTypeModel;
    /**
     * @var mixed|null
     */
    protected $entityIdentifier;
    /**
     * @var string|null
     */
    protected $attributeCode;
    /**
     * @var string|null
     */
    protected $attributeValue;
    /**
     * @var mixed|null
     */
    protected $storeId;
    /**
     * @var null
     */
    protected $attributeModel;
    /**
     * @var null
     */
    protected $objectEntity;
    /**
     * @var null
     */
    protected $_objectEntityId;
    /**
     * @var ProductFactory
     */
    protected $productFactory;
    /**
     * @var Registry
     */
    protected $registry;
    /**
     * @var \FiloBlu\Flow\Helper\Attribute
     */
    protected $helperAttribute;
    /**
     * @var \FiloBlu\Flow\Helper\Monolog\Logger|Logger
     */
    protected $logger;
    /**
     * @var ObjectManagerInterface
     */
    protected $objectManager;

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

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

    /**
     * @var CollectionFactory
     */
    protected $attributeOptionCollectionFactory;

    /**
     * @var TypeFactory
     */
    protected $entityTypeFactory;
    /**
     * @var BooleanFactory
     */
    protected $booleanFactory;

    /**
     * AbstractEntity constructor.
     * @param Registry $registry
     * @param ProductFactory $productFactory
     * @param \FiloBlu\Flow\Helper\Product $helperProduct
     * @param \FiloBlu\Flow\Helper\Attribute $helperAttribute
     * @param LoggerProvider $loggerProvider
     * @param AttributeFactory $attributeFactory
     * @param CollectionFactory $attributeOptionCollectionFactory
     * @param ObjectManagerInterface $objectManager
     * @param TypeFactory $entityTypeFactory
     * @param BooleanFactory $booleanFactory
     * @param array $data
     */
    public function __construct(

        Registry                       $registry,
        ProductFactory                 $productFactory,
        \FiloBlu\Flow\Helper\Product   $helperProduct,
        \FiloBlu\Flow\Helper\Attribute $helperAttribute,
        LoggerProvider                 $loggerProvider,
        AttributeFactory               $attributeFactory,
        CollectionFactory              $attributeOptionCollectionFactory,
        ObjectManagerInterface         $objectManager,
        TypeFactory                    $entityTypeFactory,
        BooleanFactory                 $booleanFactory,
        array                          $data = []
    )
    {
        $this->attributeOptionCollectionFactory = $attributeOptionCollectionFactory;
        $this->attributeFactory = $attributeFactory;
        $this->helperProduct = $helperProduct;
        $this->objectManager = $objectManager;
        $this->storeId = $data['storeId'];
        $this->attributeCode = trim($data['attributeCode']); // a
        $this->attributeValue = (!is_array($data['attributeValue'])) ? trim($data['attributeValue']) : $data['attributeValue']; // v
        $this->entityIdentifier = $data['entityIdentifier']; // e
        $this->entityTypeCode = $data['entityTypeCode']; // valori: catalog_product, product_attribute
        $this->productFactory = $productFactory;
        $this->registry = $registry;
        $this->helperAttribute = $helperAttribute;
        $this->entityTypeFactory = $entityTypeFactory;
        $this->logger = $loggerProvider->getLogger();
        $this->booleanFactory = $booleanFactory;
    }

    /**
     * @throws Exception
     */
    public function process()
    {
        if ($this->isSpecialFunction($this->attributeCode)) {
            $this->processSpecialFunction();
            return;
        }

        $this->setObjectAttributeValue();
    }

    /**
     * @param string $attribute
     * @return bool
     */
    public function isSpecialFunction($attribute)
    {
        return (strncmp($attribute, '_', 1) === 0);
    }

    /**
     * @return mixed
     * @throws Exception
     */
    protected function processSpecialFunction()
    {
        $specificSpecialClass = $this->objectManager->create($this->getSpecialActionClassName());

        if (!method_exists($specificSpecialClass, 'process')) {
            throw new Exception('Missing special function for attribute ' . $this->attributeCode);
        }

        return $specificSpecialClass->process($this->getObject(), $this->attributeValue, $this->storeId);
    }

    /**
     * TODO: resolve class instance from di.xml @see \FiloBlu\Flow\Model\SpecialActionPool
     * @return string
     */
    protected function getSpecialActionClassName()
    {
        return '\FiloBlu\Flow\Model\Actions\From\\' . ucfirst(str_replace('_', '', $this->attributeCode));
    }

    /**
     * @return mixed
     */
    abstract protected function getObject();

    /**
     * @throws Exception
     */
    protected function setObjectAttributeValue()
    {
        $object = $this->getObject();
        $attributeValue = $this->getAttributeValue();
        $input_type = strtolower($this->getAttribute()->getFrontend()->getInputType());

        if (empty($attributeValue) && in_array($input_type,['multiselect', 'select'])){
            return;
        }

        $object->setDataUsingMethod($this->attributeCode, $attributeValue);
        $object->getResource()->saveAttribute($object, $this->attributeCode);
    }

    /**
     * @return string
     * @throws Exception
     */
    protected function getAttributeValue()
    {
        $attribute_model = $this->getAttribute();
        $attributeValue = $this->attributeValue;
        $input_type = strtolower($attribute_model->getFrontend()->getInputType());
        $attributeValueLabels = [];

        if ($input_type === 'multiselect') {
            if (strpos($attributeValue, '|') !== false) {
                //if there is | as separator, we need to replace with ,
                $attributeValue = trim($attributeValue, '|');
                $attributeValue = trim($attributeValue);
                $attributeValue = str_replace('|', ',', $attributeValue);
            }
            $attributeValueLabels = explode(',', $attributeValue);
            $attributeValueLabels = array_map('trim', $attributeValueLabels);
            $attributeValueLabels = array_unique($attributeValueLabels);
        } else {
            $attributeValueLabels[] = $attributeValue;
        }
        return $this->findOptionValuesByLabels($attribute_model, $attributeValueLabels);
    }

    /**
     * @return \Magento\Eav\Model\Entity\Attribute|null |null
     * @throws LocalizedException
     * @throws Exception
     */
    protected function getAttribute()
    {
        if (empty($this->attributeModel)) {
            $this->attributeModel = $this->attributeFactory->create()->loadByCode($this->getEntityTypeCode(), $this->getAttributeCode());
            if (!$this->attributeModel->getId()) {
                throw new Exception("No attribute found for code {$this->attributeCode}");
            }
        }
        return $this->attributeModel;
    }

    /**
     * @return mixed|null
     * @throws Exception
     */
    public function getEntityTypeCode()
    {
        if (empty($this->entityTypeCode)) {
            throw new Exception('EntityTypeCode was not defined for this class');
        }

        return $this->entityTypeCode;
    }

    /**
     * @return string|null
     * @throws Exception
     */
    public function getAttributeCode()
    {
        if (empty($this->attributeCode)) {
            throw new Exception('AttributeCode was not defined for this class');
        }

        return $this->attributeCode;
    }

    /**
     * @param \Magento\Eav\Model\Entity\Attribute $attribute
     * @param array $values
     * @return string
     * @throws Exception
     */
    protected function findOptionValuesByLabels($attribute, $values = []): string
    {

        /*
         *  TODO: There are some crazy stuff like this \Magento\Bundle\Ui\DataProvider\Product\Form\Modifier\BundleSku where
         *       'Yes' becomes 0 And 'No' = 1. Find a right way to manage this situations.
         *       $resolved = $attribute->getFrontend()->getValue(new DataObject([$attribute->getAttributeCode() => __($value)]));

         */

        $filteredValues = array_unique($values);
        $optionsToAdd = [];
        $attributeValueArray = [];
        $isBoolean = $attribute->getFrontend()->getConfigField('input') === 'boolean';

        if ($isBoolean || $attribute->usesSource()) {
            foreach ($filteredValues as $label) {
                $result = $this->getMatchingOption($attribute, $label, $isBoolean);
                if ($result !== null) {
                    $attributeValueArray[] = $result;
                    $optionsToAdd[$label] = true;
                    continue;
                }
                $optionsToAdd[$label] = false;
            }

            /** Add missing options for used defined attributes */
            if ((int)$this->storeId === 0 && $attribute->getIsUserDefined() && count($filteredValues) !== count($attributeValueArray)) {
                foreach ($optionsToAdd as $newLabel => $exists) {
                    if ($exists) {
                        continue;
                    }

                    $attributeValueArray[] = $this->addAttributeOptionValue($newLabel);
                }
            }
        } else {
            $attributeValueArray = $filteredValues;
        }

        return implode(',', $attributeValueArray);
    }

    /**
     *
     * @param $attribute
     * @param $needle
     * @param $isBoolean
     * @return mixed|null
     */
    protected function getMatchingOption($attribute, $needle, $isBoolean)
    {

        // We need to execute the search
        // only on the attribute admin values
        $attribute->setStoreId(0);

        $options = $attribute->getSource()->getAllOptions();
        $matchBy = in_array($attribute->getAttributeCode(), $this->attributeMappedByValue) ? 'value' : 'label';
        foreach ($options as $option) {
            $label = (string)$option[$matchBy];
            if ($needle === $label) {
                return $option['value'];
            }
        }

        // TODO : simplify
        $options = $this->booleanFactory->create()->getAllOptions();
        foreach ($options as $option) {
            $label = (string)$option[$matchBy];
            if ($needle === $label) {
                return $option['value'];
            }

            if ($isBoolean && $needle == $option['value']) {
                return $option['value'];
            }
        }

        return null;
    }

    /**
     * @param $attributeValueLabel
     * @return bool
     * @throws Exception
     */
    protected function addAttributeOptionValue($attributeValueLabel)
    {
        try {
            return $this->helperAttribute->addAttributeOption(
                [
                    'attribute_id' => $this->getAttribute()->getId(),
                    'store_id'     => 0,
                    'value'        => $attributeValueLabel
                ]
            );
        } catch (Exception $e) {
            throw new Exception("Couldn't add option {$attributeValueLabel} for attribute {$this->attributeCode} entity type " . $this->getEntityTypeCode());
        }
    }

    /**
     * @return mixed|null
     */
    public function getObjectId()
    {
        if ($this->_objectEntityId === null) {
            $this->_objectEntityId = $this->entityIdentifier;
        }
        return $this->_objectEntityId;
    }

    /**
     * @return string|null
     * @throws Exception
     */
    protected function getEntityModel()
    {
        if ($this->entityTypeModel === null) {
            $entityType = $this->entityTypeFactory->create()->loadByCode($this->getEntityTypeCode());

            if (!$entityType->getId()) {
                throw new RuntimeException('EntityType not found for code ' . $this->entityTypeCode);
            }

            $this->entityTypeModel = $entityType->getEntityModel();
        }

        return $this->entityTypeModel;
    }
}
