<?php
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */
declare(strict_types=1);

namespace Magento\Setup\Fixtures;

use Magento\Catalog\Model\Product;
use Magento\Catalog\Model\Product\Attribute\Source\Status;
use Magento\Catalog\Model\ProductFactory;
use Magento\Eav\Model\Entity\Attribute;
use Magento\Eav\Model\ResourceModel\Entity\Attribute\Collection;
use Magento\Eav\Model\ResourceModel\Entity\Attribute\CollectionFactory;
use Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\Collection as AttributeSetCollection;
use Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory as AttributeSetCollectionFactory;
use Magento\Setup\Model\FixtureGenerator\ProductGenerator;
use Magento\Setup\Model\SearchTermDescriptionGeneratorFactory;

/**
 * Generate simple products based on profile configuration
 * Support the following format:
 * <simple_products>{products amount}</simple_products>
 * Products will be distributed per Default and pre-defined
 * attribute sets (@see setup/performance-toolkit/config/attributeSets.xml)
 *
 * If extra attribute set is specified in profile as: <product_attribute_sets>{sets amount}</product_attribute_sets>
 * then products also will be distributed per additional attribute sets
 *
 * Products will be uniformly distributed per categories and websites
 * If node "assign_entities_to_all_websites" from profile is set to "1" then products will be assigned to all websites
 *
 * @see setup/performance-toolkit/profiles/ce/small.xml
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
 */
class SimpleProductsFixture extends Fixture
{
    /**
     * Simple product sku pattern
     */
    public const SKU_PATTERN = 'product_dynamic_%s';

    /**
     * @var int
     */
    protected $priority = 31;

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

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

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

    /**
     * @var ProductGenerator
     */
    private $productGenerator;

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

    /**
     * @var Collection
     */
    private $attributeCollectionFactory;

    /**
     * @var AttributeSetCollectionFactory
     */
    private $attributeSetCollectionFactory;

    /**
     * @var SearchTermDescriptionGeneratorFactory
     */
    private $descriptionGeneratorFactory;

    /**
     * @var ProductsAmountProvider
     */
    private $productsAmountProvider;

    /**
     * @var WebsiteCategoryProvider
     */
    private $websiteCategoryProvider;

    /**
     * @var PriceProvider
     */
    private $priceProvider;

    /**
     * @var int[]
     */
    private $additionalAttributeSetIds;

    /**
     * @param FixtureModel $fixtureModel
     * @param ProductFactory $productFactory
     * @param ProductGenerator $productGenerator
     * @param CollectionFactory $attributeCollectionFactory
     * @param AttributeSetCollectionFactory $attributeSetCollectionFactory
     * @param SearchTermDescriptionGeneratorFactory $descriptionGeneratorFactory
     * @param WebsiteCategoryProvider $websiteCategoryProvider
     * @param ProductsAmountProvider $productsAmountProvider
     * @param PriceProvider $priceProvider
     * @internal param FixtureConfig $fixtureConfig
     */
    public function __construct(
        FixtureModel $fixtureModel,
        ProductFactory $productFactory,
        ProductGenerator $productGenerator,
        CollectionFactory $attributeCollectionFactory,
        AttributeSetCollectionFactory $attributeSetCollectionFactory,
        SearchTermDescriptionGeneratorFactory $descriptionGeneratorFactory,
        WebsiteCategoryProvider $websiteCategoryProvider,
        ProductsAmountProvider $productsAmountProvider,
        PriceProvider $priceProvider
    ) {
        parent::__construct($fixtureModel);
        $this->productFactory = $productFactory;
        $this->productGenerator = $productGenerator;
        $this->attributeCollectionFactory = $attributeCollectionFactory;
        $this->attributeSetCollectionFactory = $attributeSetCollectionFactory;
        $this->descriptionGeneratorFactory = $descriptionGeneratorFactory;
        $this->productsAmountProvider = $productsAmountProvider;
        $this->websiteCategoryProvider = $websiteCategoryProvider;
        $this->priceProvider = $priceProvider;
    }

    /**
     * @inheritdoc
     */
    public function getActionTitle()
    {
        return 'Generating simple products';
    }

    /**
     * @inheritdoc
     */
    public function introduceParamLabels()
    {
        return [
            'simple_products' => 'Simple products'
        ];
    }

    /**
     * @inheritdoc
     *
     * @SuppressWarnings(PHPMD)
     */
    public function execute()
    {
        $simpleProductsCount = $this->productsAmountProvider->getAmount(
            $this->fixtureModel->getValue('simple_products', 0),
            $this->getSkuPattern()
        );

        if (!$simpleProductsCount) {
            return;
        }

        $defaultAttributeSets = $this->getDefaultAttributeSets();
        $searchTermsConfig = $this->getSearchTerms();

        /** @var \Magento\Setup\Model\SearchTermDescriptionGenerator $descriptionGenerator */
        $descriptionGenerator = $this->descriptionGeneratorFactory->create(
            $this->getDescriptionConfig(),
            $searchTermsConfig,
            $simpleProductsCount,
            'Full simple product Description %s'
        );

        /** @var \Magento\Setup\Model\SearchTermDescriptionGenerator $shortDescriptionGenerator */
        $shortDescriptionGenerator = $this->descriptionGeneratorFactory->create(
            $this->getShortDescriptionConfig(),
            $searchTermsConfig,
            $simpleProductsCount,
            'Short simple product Description %s'
        );

        $additionalAttributeSetIds = $this->getAdditionalAttributeSetIds();
        $attributeSet = function ($index) use ($defaultAttributeSets, $additionalAttributeSetIds) {
            // phpcs:ignore
            mt_srand($index);
            $attributeSetCount = count(array_keys($defaultAttributeSets));
            if ($attributeSetCount > (($index - 1) % (int)$this->fixtureModel->getValue('categories', 30))) {
                // phpcs:ignore Magento2.Security.InsecureFunction
                return array_keys($defaultAttributeSets)[mt_rand(0, count(array_keys($defaultAttributeSets)) - 1)];
            } else {
                $customSetsAmount = count($additionalAttributeSetIds);
                return $customSetsAmount
                    ? $additionalAttributeSetIds[$index % $customSetsAmount]
                    : $this->getDefaultAttributeSetId();
            }
        };

        $additionalAttributeValues = $this->getAdditionalAttributeValues();
        $additionalAttributes = function (
            $attributeSetId,
            $index
        ) use (
            $defaultAttributeSets,
            $additionalAttributeValues
        ) {
            $attributeValues = [];
            // phpcs:ignore
            mt_srand($index);
            $attributeValuesByAttributeSet = $defaultAttributeSets[$attributeSetId]
                ?? $additionalAttributeValues[$attributeSetId];
            if (!empty($attributeValuesByAttributeSet)) {
                foreach ($attributeValuesByAttributeSet as $attributeCode => $values) {
                    // phpcs:ignore Magento2.Security.InsecureFunction
                    $attributeValues[$attributeCode] = $values[mt_rand(0, count($values) - 1)];
                }
            }

            return $attributeValues;
        };

        $fixtureMap = [
            'name' => function ($productId) {
                return sprintf('Simple Product %s', $productId);
            },
            'sku' => function ($productId) {
                return sprintf($this->getSkuPattern(), $productId);
            },
            'price' => function ($index, $entityNumber) {
                return $this->priceProvider->getPrice($entityNumber);
            },
            'url_key' => function ($productId) {
                return sprintf('simple-product-%s', $productId);
            },
            'description' => function ($index) use ($descriptionGenerator) {
                return $descriptionGenerator->generate($index);
            },
            'short_description' => function ($index) use ($shortDescriptionGenerator) {
                return $shortDescriptionGenerator->generate($index);
            },
            'website_ids' => function ($index, $entityNumber) {
                return $this->websiteCategoryProvider->getWebsiteIds($index);
            },
            'category_ids' => function ($index, $entityNumber) {
                return $this->websiteCategoryProvider->getCategoryId($index);
            },
            'meta_keyword' => function ($productId) {
                return sprintf($this->getSkuPattern(), $productId);
            },
            'meta_title' => function ($productId) {
                return sprintf($this->getSkuPattern(), $productId);
            },
            'attribute_set_id' => $attributeSet,
            'additional_attributes' => $additionalAttributes,
            'status' => function () {
                return Status::STATUS_ENABLED;
            }
        ];
        $this->productGenerator->generate($simpleProductsCount, $fixtureMap);
    }

    /**
     * Get simple product sku pattern
     *
     * @return string
     */
    private function getSkuPattern()
    {
        return self::SKU_PATTERN;
    }

    /**
     * Get default attribute set id
     *
     * @return int
     */
    private function getDefaultAttributeSetId()
    {
        if (null === $this->defaultAttributeSetId) {
            $this->defaultAttributeSetId = (int)$this->productFactory->create()->getDefaultAttributeSetId();
        }

        return $this->defaultAttributeSetId;
    }

    /**
     * Get default attribute sets with attributes.
     *
     * @return array
     * @see config/attributeSets.xml
     */
    private function getDefaultAttributeSets()
    {
        $attributeSets = $this->fixtureModel->getValue('attribute_sets', null);
        $attributes = [];

        if ($attributeSets !== null && array_key_exists('attribute_set', $attributeSets)) {
            foreach ($attributeSets['attribute_set'] as $attributeSet) {
                $attributesData = array_key_exists(0, $attributeSet['attributes']['attribute'])
                    ? $attributeSet['attributes']['attribute'] : [$attributeSet['attributes']['attribute']];

                $attributeCollection = $this->attributeCollectionFactory->create();

                $attributeCollection->setAttributeSetFilterBySetName($attributeSet['name'], Product::ENTITY);
                $attributeCollection->addFieldToFilter(
                    'attribute_code',
                    array_column($attributesData, 'attribute_code')
                );
                $attributes = $this->processAttributeValues($attributeCollection, $attributes);
            }
        }
        $attributes[$this->getDefaultAttributeSetId()] = [];

        return $attributes;
    }

    /**
     * Get search terms config which used for product description generation
     *
     * @return array
     */
    private function getSearchTerms()
    {
        $searchTerms = $this->fixtureModel->getValue('search_terms', []);
        if (!empty($searchTerms)) {
            $searchTerms = array_key_exists(0, $searchTerms['search_term'])
                ? $searchTerms['search_term'] : [$searchTerms['search_term']];
        }

        return $searchTerms;
    }

    /**
     * Get description config
     *
     * @return array
     */
    private function getDescriptionConfig()
    {
        if (null === $this->descriptionConfig) {
            $this->descriptionConfig = $this->readDescriptionConfig('description');
        }

        return $this->descriptionConfig;
    }

    /**
     * Get short description config
     *
     * @return array
     */
    private function getShortDescriptionConfig()
    {
        if (null === $this->shortDescriptionConfig) {
            $this->shortDescriptionConfig = $this->readDescriptionConfig('short-description');
        }

        return $this->shortDescriptionConfig;
    }

    /**
     * Get description config from fixture
     *
     * @param string $configSrc
     * @return array
     */
    private function readDescriptionConfig($configSrc)
    {
        $configData = $this->fixtureModel->getValue($configSrc, []);

        if (isset($configData['mixin']['tags'])) {
            $configData['mixin']['tags'] = explode('|', $configData['mixin']['tags']);
        }

        return $configData;
    }

    /**
     * Get additional attribute set ids.
     *
     * @return int[]
     */
    private function getAdditionalAttributeSetIds()
    {
        if (null === $this->additionalAttributeSetIds) {
            /** @var AttributeSetCollection $sets */
            $sets = $this->attributeSetCollectionFactory->create();
            $sets->addFieldToFilter(
                'attribute_set_name',
                ['like' => AttributeSetsFixture::PRODUCT_SET_NAME . '%']
            );
            $this->additionalAttributeSetIds = $sets->getAllIds();
        }

        return $this->additionalAttributeSetIds;
    }

    /**
     * Get values of additional attributes.
     *
     * @return array
     */
    private function getAdditionalAttributeValues(): array
    {
        $attributeCollection = $this->attributeCollectionFactory->create();
        $attributeCollection->setAttributeSetsFilter($this->getAdditionalAttributeSetIds())
            ->addFieldToFilter('attribute_code', ['like' => 'attribute_set%']);
        $attributeCollection->getSelect()->columns(['entity_attribute.attribute_set_id']);

        return $this->processAttributeValues($attributeCollection);
    }

    /**
     * Maps attribute values by attribute set and attribute code.
     *
     * @param Collection $attributeCollection
     * @param array $attributes
     * @return array
     */
    private function processAttributeValues(
        Collection $attributeCollection,
        array $attributes = []
    ): array {
        /** @var Attribute $attribute */
        foreach ($attributeCollection as $attribute) {
            $values = [];
            $options = $attribute->getOptions() ?? [];
            $attributeSetId = $attribute->getAttributeSetId() ?? $this->getDefaultAttributeSetId();
            foreach ($options as $option) {
                if ($option->getValue()) {
                    $values[] = $option->getValue();
                }
            }
            $attributes[$attributeSetId][$attribute->getAttributeCode()] = $values;
        }

        return $attributes;
    }
}
