<?php

declare(strict_types=1);

namespace Iazel\RegenProductUrl\Service;

use Exception;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Model\Product;
use Magento\Catalog\Model\Product\Attribute\Source\Status;
use Magento\Catalog\Model\Product\Visibility;
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory;
use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator;
use Magento\Store\Api\Data\StoreInterface;
use Magento\Store\Model\Store;
use Magento\Store\Model\StoreManagerInterface;
use Magento\UrlRewrite\Model\UrlPersistInterface;
use Magento\UrlRewrite\Service\V1\Data\UrlRewrite;
use Symfony\Component\Console\Output\OutputInterface;

class RegenerateProductUrl
{
    const BATCH_SIZE = 500;

    /**
     * @var OutputInterface|null
     */
    private $output;

    /**
     * @var CollectionFactory
     */
    private $collectionFactory;

    /**
     * @var ProductUrlRewriteGenerator
     */
    private $urlRewriteGenerator;

    /**
     * @var UrlPersistInterface
     */
    private $urlPersist;

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

    /**
     * Counter for amount of urls regenerated.
     * @var int
     */
    private $regeneratedCount = 0;

    /**
     * Constructor.
     *
     * @param CollectionFactory $collectionFactory
     * @param ProductUrlRewriteGenerator $urlRewriteGenerator
     * @param UrlPersistInterface $urlPersist
     * @param StoreManagerInterface $storeManager
     */
    public function __construct(
        CollectionFactory $collectionFactory,
        ProductUrlRewriteGenerator $urlRewriteGenerator,
        UrlPersistInterface $urlPersist,
        StoreManagerInterface $storeManager
    ) {
        $this->collectionFactory   = $collectionFactory;
        $this->urlRewriteGenerator = $urlRewriteGenerator;
        $this->urlPersist          = $urlPersist;
        $this->storeManager        = $storeManager;
    }

    /**
     * @param int[]|nul $productIds
     * @param int|nul $storeId
     * @param bool $verbose
     *
     * @return void
     * @throws \Magento\Framework\Exception\NoSuchEntityException
     */
    public function execute(?array $productIds = null, ?int $storeId = null, bool $verbose = false): void
    {
        $this->regeneratedCount = 0;

        $stores = !is_null($storeId)
            ? [$this->storeManager->getStore($storeId)]
            : $this->storeManager->getStores(false);

        foreach ($stores as $store) {
            $regeneratedForStore = 0;

            $this->log(sprintf('Start regenerating for store %s (%d)', $store->getName(), $store->getId()));

            $collection = $this->collectionFactory->create();
            $collection
                ->setStoreId($store->getId())
                ->addStoreFilter($store->getId())
                ->addAttributeToSelect('name')
                ->addFieldToFilter('status', ['eq' => Status::STATUS_ENABLED])
                ->addFieldToFilter('visibility', ['gt' => Visibility::VISIBILITY_NOT_VISIBLE]);

            if (!is_null($productIds)) {
                $productIds = $collection->getAllIds();
            }
            $collection->addIdFilter($productIds);

            $collection->addAttributeToSelect(['url_path', 'url_key']);

            $deleteProducts = [];

            /** @var Product $product */
            foreach ($collection as $product) {
                $deleteProducts[] = $product->getId();
                if (count($deleteProducts) >= self::BATCH_SIZE) {
                    $this->deleteUrls($deleteProducts, $store);
                }
            }

            if (count($deleteProducts)) {
                $this->deleteUrls($deleteProducts, $store, true);
            }

            $newUrls = [];
            try {
                /** @var Product $product */
                foreach ($collection as $product) {
                    if ($verbose) {
                        $this->log(
                            sprintf(
                                'Regenerating urls for %s (%s) in store (%s)',
                                $product->getSku(),
                                $product->getId(),
                                $store->getName()
                            )
                        );
                    }

                    $product->setStoreId($store->getId());

                    $newUrls = array_merge($newUrls, $this->urlRewriteGenerator->generate($product));
                    if (count($newUrls) >= self::BATCH_SIZE) {
                        $regeneratedForStore += $this->replaceUrls($newUrls);
                    }
                }

                if (count($newUrls)) {
                    $regeneratedForStore += $this->replaceUrls($newUrls, true);
                }
            } catch (Exception $e) {
                $this->log(
                    sprintf(
                        '<error>Duplicated url for store ID %d, product %d (%s) - %s Generated URLs:' .
                        PHP_EOL . '%s</error>' . PHP_EOL,
                        $store->getId(),
                        $product->getId(),
                        $product->getSku(),
                        $e->getMessage(),
                        implode(PHP_EOL, array_keys($newUrls))
                    )
                );
            }

            $this->log(
                sprintf(
                    'Done regenerating. Regenerated %d urls for store %s (%d)',
                    $regeneratedForStore,
                    $store->getName(),
                    $store->getId()
                )
            );
            $this->regeneratedCount += $regeneratedForStore;
        }
    }

    /**
     * @param OutputInterface $output
     *
     * @return void
     */
    public function setOutput(OutputInterface $output): void
    {
        $this->output = $output;
    }

    /**
     * @return int
     */
    public function getRegeneratedCount(): int
    {
        return $this->regeneratedCount;
    }

    /**
     * @param string $message
     *
     * @return void
     */
    private function log(string $message): void
    {
        if ($this->output !== null) {
            $this->output->writeln($message);
        }
    }

    /**
     * Add product ulrs
     *
     * @param array $urls
     * @param bool $last
     *
     * @return int
     * @throws \Magento\UrlRewrite\Model\Exception\UrlAlreadyExistsException
     */
    private function replaceUrls(array &$urls, bool $last = false): int
    {
        $this->log(sprintf('replaceUrls%s batch: %d', $last ? ' last' : '', count($urls)));
        $this->urlPersist->replace($urls);
        $count = count($urls);
        $urls  = [];

        return $count;
    }

    /**
     * Remove old product urls
     *
     * @param array $productIds
     * @param StoreInterface $store
     * @param bool $last
     *
     * @return int
     */
    private function deleteUrls(array &$productIds, StoreInterface $store, bool $last = false): int
    {
        $this->log(sprintf('deleteUrls%s batch: %d', $last ? 'last' : '', count($productIds)));
        $this->urlPersist->deleteByData([
            UrlRewrite::ENTITY_ID     => $productIds,
            UrlRewrite::ENTITY_TYPE   => ProductUrlRewriteGenerator::ENTITY_TYPE,
            UrlRewrite::REDIRECT_TYPE => 0,
            UrlRewrite::STORE_ID      => $store->getId()
        ]);
        $count      = count($productIds);
        $productIds = [];

        return $count;
    }
}
