<?php

declare(strict_types=1);

namespace External\PompoBundle\Util;

use KupShop\CatalogBundle\ProductList\MultiFetch;
use KupShop\CatalogBundle\ProductList\ProductCollection;
use KupShop\CatalogBundle\ProductList\ProductList;
use KupShop\KupShopBundle\Context\CurrencyContext;
use KupShop\KupShopBundle\Context\UserContext;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\KupShopBundle\Util\DateUtil;
use KupShop\KupShopBundle\Util\Functional\Mapping;
use KupShop\KupShopBundle\Util\Price\Price;
use KupShop\KupShopBundle\Util\Price\PriceCalculator;
use KupShop\KupShopBundle\Util\Price\ProductPrice;
use KupShop\LabelsBundle\Util\LabelUtil;
use KupShop\SellerBundle\Context\SellerContext;
use Query\Operator;
use Query\Product;

class ProductUtil
{
    private const int MAIN_STORE_ID = 2;
    private const int SUPPLIER_ID = 3;

    public function __construct(
        private Configuration $configuration,
        private ProductList $productList,
        private LabelUtil $labelUtil,
    ) {
    }

    /**
     * Multifetch pro nafetchovani dalsich cen, ktere potrebujeme zobrazovat na katalogu / detailu produktu.
     *
     * Nafetchuje DMOC cenu, VIP cenu a "Pompo klub setri" cenu.
     * Zaroven to prida dmocDiscount, coz je sleva vypocitana z DMOC ceny.
     */
    public function fetchAdditionalPrices(ProductCollection $products): void
    {
        if ($products->count() === 0) {
            return;
        }

        if (isset($products->_pompoVipPricesFetched)) {
            return;
        }

        $products->fetchPricelists();
        $this->fetchDefaultPriceForDiscount($products);

        $currencyContext = Contexts::get(CurrencyContext::class);

        // pokud existuje cenik s DMOC, tak ho nactu
        if ($dmocPriceListId = ($this->configuration->getDMOCPriceLists()[$currencyContext->getActiveId()] ?? false)) {
            foreach ($products as $product) {
                $product->dmocPrice = $product->pricelists[$dmocPriceListId]['productPrice'] ?? null;
                $product->dmocDiscount = \DecimalConstants::zero();
                if ($product->dmocPrice) {
                    $product->dmocPrice = PriceCalculator::convert($product->dmocPrice, $product->getProductPrice()->getCurrency());
                    $productPriceWithVat = $product->getProductPrice()->getPriceWithVat();
                    $dmocPriceWithVat = $product->dmocPrice->getPriceWithVat();
                    if ($dmocPriceWithVat->isPositive()) {
                        $product->dmocDiscount = \DecimalConstants::one()->sub($productPriceWithVat->div($dmocPriceWithVat))->mul(\DecimalConstants::hundred());
                    } else {
                        $product->dmocDiscount = \DecimalConstants::zero();
                    }
                }
            }
        }

        // pokud existuje cenik pro prihlaseneho uzivatele, tak nactu VIP cenu
        if ($vipPriceListId = ($this->configuration->getUsersPriceLists()[$currencyContext->getActiveId()] ?? false)) {
            $userIsActive = Contexts::get(UserContext::class)->isActive();
            foreach ($products as $product) {
                $product->userVipPrice = $product->pricelists[$vipPriceListId]['productPrice'] ?? null;
                $product->vipOriginalPrice = null;

                // pokud jsem prihlasenej a existuje VIP cena, tak chci nacist "Pompo setri" cenu
                if ($userIsActive && $product->userVipPrice) {
                    $userVipPrice = $product->userVipPrice;
                    // puvodni cena produktu (neprihlaseny uzivatel)
                    // pokud mam puvodni cenu brat z ceniku
                    if ($originalPriceListId = ($this->configuration->getPriceLists()[$currencyContext->getActiveId()] ?? false)) {
                        $originalPrice = $product->pricelists[$originalPriceListId]['productPrice'] ?? $product->priceOriginal->getObject();
                        $defaultPriceForDiscount = $originalPrice->getPriceWithoutDiscount();
                        if (!empty($product->pricelists[$originalPriceListId]['price_for_discount'])) {
                            $defaultPriceForDiscount = new ProductPrice(
                                toDecimal($product->pricelists[$originalPriceListId]['price_for_discount']),
                                Contexts::get(CurrencyContext::class)->getOrDefault($product->pricelists[$originalPriceListId]['currency']),
                                $product->vat
                            );
                        }
                    } else {
                        $originalPrice = $product->priceOriginal->getObject();
                        $defaultPriceForDiscount = $product->defaultPriceForDiscount ?? $originalPrice->getPriceWithoutDiscount();
                    }

                    // prevod cen na stejnou menu
                    PriceCalculator::makeCompatible($userVipPrice, $originalPrice, false);

                    // pokud je VIP cena i original cena stejna
                    $vipSavingsEnabled = true;
                    if ($userVipPrice->getPriceWithVat()->equals($originalPrice->getPriceWithVat())) {
                        $originalPrice = $defaultPriceForDiscount;
                        $vipSavingsEnabled = false;
                    }

                    // prevod cen na stejnou menu
                    PriceCalculator::makeCompatible($userVipPrice, $originalPrice, false);

                    $product->priceForDiscount = $originalPrice;
                    $product->originalPrice = $originalPrice;
                    $product->vipOriginalPrice = $originalPrice;

                    // spocitam rozdil - tim zjistim kolik mi setri Pompo klub
                    $diff = $originalPrice->getPriceWithVat()->sub($userVipPrice->getPriceWithVat());
                    $product->vipSavingsPrice = null;
                    // pokud je rozdil vetsi jak 0, tak ho ulozim at ho muzu zobrazit
                    if ($vipSavingsEnabled && $diff->isPositive()) {
                        $product->vipSavingsPrice = new Price($diff, $userVipPrice->getCurrency(), 0);
                    }
                }
            }
        }

        $products->_pompoVipPricesFetched = true;
    }

    public function fetchDefaultPriceForDiscount(ProductCollection $products): void
    {
        if ($products->count() === 0) {
            return;
        }

        $qb = sqlQueryBuilder()
            ->select('id, COALESCE(price_for_discount, price) as price_for_discount, vat')
            ->from('products')
            ->where(Operator::inIntArray($products->getProductIds(), 'id'));

        $defaultCurrency = Contexts::get(CurrencyContext::class)->getDefault();

        foreach ($qb->execute() as $item) {
            if (isset($products[$item['id']]) && !empty($item['price_for_discount'])) {
                $price_for_discount = toDecimal($item['price_for_discount']);
                $products[$item['id']]->defaultPriceForDiscount = new ProductPrice($price_for_discount, $defaultCurrency, getVat($item['vat']));
            }
        }
    }

    /**
     * Multifetch pro nafetchovani informaci okolo skladu / prodejen, ktere se pouzivaji v katalogu, v kosiku atd...
     *
     * Nafetchuje skladovosti rozdelene podle skladu - jde o centralni sklad, sklad aktualne vybrane prodejny a soucet kusu na ostatnich prodejnach.
     */
    public function fetchStoreInfo(ProductCollection $products): void
    {
        if (!findModule(\Modules::SELLERS) || $products->count() === 0) {
            return;
        }

        static $storeIdBySeller = [];

        $sellerContext = Contexts::get(SellerContext::class);

        // nacist ID aktivniho skladu podle aktivni prodejny
        if (!($storeIdBySeller[$sellerContext->getActiveId()] ?? false)) {
            $storeId = sqlQueryBuilder()
                ->select('id_store')
                ->from('sellers')
                ->where(Operator::equals(['id' => $sellerContext->getActiveId()]))
                ->execute()->fetchOne();

            if ($storeId) {
                $storeIdBySeller[$sellerContext->getActiveId()] = (int) $storeId;
            }
        }

        $activeStoreId = $storeIdBySeller[$sellerContext->getActiveId()] ?? null;

        // nemusim to fetchovat znova, protoze uz to je fetchnuty
        if ($products->_pompoStoresFetched[$activeStoreId] ?? false) {
            return;
        }

        // multifetchu informace o skaldech
        $products->fetchStoresInStore();
        $products->fetchProductOfSuppliersInStore();

        $sellersByStoreId = $this->getSellersByStoreId();

        // k produktum doplnim potrebne informace
        foreach ($products as $product) {
            // skladovost na centralnim skladu
            $product->inStoreMain = (int) ($product->storesInStore[$this->configuration->getMainStoreId()]['in_store'] ?? 0);
            // skladovost u dodavatele
            $product->inStoreSupplier = (int) ($product->in_store_suppliers ?? 0);
            // skladovost na moji prodejne
            $product->inStoreSeller = (int) ($product->storesInStore[$activeStoreId]['in_store'] ?? 0);
            // skladovost na ostatnich prodejnach
            $inStoreSellerOther = 0;
            foreach ($product->storesInStore ?? [] as $storeId => $item) {
                // v $sellersByStoreId mam prodejce indexovany podle ID skladu
                // zaroven tam jsou odfiltrovany prodejny podle aktivniho jazyku - pro cs jen CZ prodejny a pro sk jen SK prodejny
                // takze na CZ shopu nebudeme zobrazovat skladovost SK prodejen, protoze SK prodejna stejne na CZ shopu nejde vybrat
                if (!($sellersByStoreId[$storeId] ?? false)) {
                    continue;
                }

                $inStoreSellerOther += max(0, $item['in_store']);
            }
            $product->inStoreSellerOther = (int) $inStoreSellerOther;
        }

        if (!isset($products->_pompoStoresFetched)) {
            $products->_pompoStoresFetched = [];
        }

        $products->_pompoStoresFetched[$activeStoreId] = true;
    }

    /**
     * Vrací, zda je daný produkt skladem u marketplace dodavatele.
     */
    public function isInStoreMarketplace(\Product $product, float $quantity): bool
    {
        $marketplaceSuppliers = $this->configuration->getMarketplaceSuppliers();
        $suppliers = $product->getSuppliers();

        foreach ($marketplaceSuppliers as $marketplaceSupplierId => $_) {
            if (($suppliers[0][$marketplaceSupplierId] ?? 0) > $quantity) {
                return true;
            }
        }

        return false;
    }

    /**
     * Vrátí datum dopravy navýšený a potřebný počet dnů.
     *
     * @deprecated `External\PompoBundle\Overrides\Order\DeliveryInfo::calculateCollectionDate` is used instead
     */
    public function getDeliveryDateIncremented(\DateTime $deliveryDate, array $items, ?\Delivery $delivery = null): \DateTime
    {
        return $deliveryDate;
    }

    public function getDeliveryExpeditionDate(\Delivery $delivery): \DateTime
    {
        // Kontroluju kvuli tomu, zda se stihne odeslat jeste dnes nebo ne
        $expeditionDate = new \DateTime();

        $timeHours = '13:00:00';
        if (!empty($delivery->time_hours)) {
            $timeHours = $delivery->time_hours;
        }

        try {
            $hourParts = explode(':', $timeHours);
            $maxHoursToday = (new \DateTime())
                ->setTime((int) ($hourParts[0] ?? 0), (int) ($hourParts[1] ?? 0), (int) ($hourParts[2] ?? 0));

            // Pokud se nestihne odeslat jeste dnes, tak navysim datum jeste o jeden den
            if ((new \DateTime()) > $maxHoursToday) {
                $expeditionDate = DateUtil::calcWorkingDays(1, $expeditionDate);
            }
        } catch (\Throwable $e) {
        }

        return $expeditionDate;
    }

    /**
     * Vrací date increment - počet dní o kolik se má navýšit datum doručení.
     *
     * Date increment se urcuje podle dodavatele, zavrene expedice z centraly nebo podle specialne nastaveneho incrementu pro dany den.
     */
    public function getDateIncrement(array $items, ?array $orderedQuantities = [], ?\DateTime $expeditionDate = null): int
    {
        $increment = 0;

        if (empty($items)) {
            return $increment;
        }

        $filterItems = [];
        foreach ($items as $key => $_) {
            $parts = explode('/', (string) $key);
            $filterItems[$parts[0]] = $filterItems[$parts[0]] ?? null;
            if ($parts[1] ?? null) {
                $filterItems[$parts[0]][] = $parts[1];
            }
        }

        // nactu si produkty
        $products = $this->getProducts($filterItems);
        // fetchnu si k produktum dodatecne info
        $this->fetchProductsSupplierDeliveryDateIncrement($products);
        $this->fetchStoreInfo($products);

        // projdu produkty
        foreach ($products as $key => $product) {
            // pokud je to nejhracka, tak nemame prodejny a zajima nas inStore field na produktu, ale pokud
            // jsme na pompu, tak nas zajima inStoreMain, kde je skladovost na hlavnim skladu bez prodejen
            $inStoreMain = $this->configuration->isNejhracka() ? $product->inStore : ($product->inStoreMain ?? 0);

            $tmpIncrement = 0;
            // pocet kusu skladem, ktere objednavam
            $requiredQuantity = $items[$key] ?? 0;
            // pocet kusu, ktere jsou objednane v uz vytvorene objednavce
            $orderedQuantity = $orderedQuantities[$key] ?? 0;
            // pocet kusu skladem na hlavnim skladu
            $inStore = $inStoreMain + min($orderedQuantity, $inStoreMain);
            // pocet kusu skladem u dodavatele
            $inStoreSuppliers = $product->in_store_suppliers;

            // navysit increment podle skladovosti - pokud neni skladem na hlavnim skladu, ale je skladem u dodavatele, tak dorucuji pozdeji
            if ($inStore < $requiredQuantity && $inStoreSuppliers > 0) {
                $tmpIncrement = (int) ($product->supplierDeliveryDateIncrement ?? 0);
                if ($supplierIncrement = $this->getSupplierOrderIncrement($product->_pompoSupplierId ?? self::SUPPLIER_ID)) {
                    $tmpIncrement += $supplierIncrement;
                }
            }

            $increment = max($increment, $tmpIncrement);
        }

        $expeditionDate ??= new \DateTime();

        // pokud je napr. expedice z centraly v dany den uzavrena, tak musim doruceni navysit
        if ($mainStoreIncrement = $this->getMainStoreDeliveryDateIncrement($expeditionDate)) {
            $increment += $mainStoreIncrement;
        }

        // pokud je v dany den nastaven nejaky dodatecny increment (Nastaveni e-shopu, Pompo a "Navýšení data doručení"
        if ($dayIncrement = $this->getDeliveryIncrementByDay($expeditionDate)) {
            $increment += $dayIncrement;
        }

        return $increment;
    }

    /**
     * Vrací datumy, kdy je hlavní sklad uzavřen a tímpádem je uzavčena i expedice z něj.
     */
    public function getMainStoreClosedDates(): array
    {
        $dbcfg = \Settings::getDefault();

        $from = $dbcfg->datago['mainStoreClosed']['from'] ?? null;
        $to = $dbcfg->datago['mainStoreClosed']['to'] ?? null;

        if (!empty($from) && !empty($to)) {
            try {
                $dateFrom = (new \DateTime($from))->setTime(0, 0);
                $dateTo = (new \DateTime($to))->setTime(23, 59, 59);

                return [$dateFrom, $dateTo];
            } catch (\Throwable $e) {
            }
        }

        return [null, null];
    }

    /**
     * Vrátí počet dní, o které je potřeba navýšit datum doručení pokud je expedice z centrály uzavřena.
     * Např. kvůli inventuře, svátku, nebo Vánocům.
     */
    public function getMainStoreDeliveryDateIncrement(\DateTime $expeditionDate): int
    {
        $increment = 0;

        [$from, $to] = $this->getMainStoreClosedDates();

        if (!empty($from) && !empty($to)) {
            try {
                // Pokud je centralni sklad z nejakyho duvodu uzavren
                if ($expeditionDate >= $from && $expeditionDate <= $to) {
                    // rozdil mezi datumama
                    $diff = (new \DateTime())->diff($to);
                    // Ensure the increment is at least 1 day
                    $increment = max(1, abs($diff->days));
                }
            } catch (\Throwable $e) {
            }
        }

        return $increment;
    }

    public function getDeliveryIncrementByDay(\DateTime $expeditionDate): int
    {
        $dbcfg = \Settings::getDefault();

        if ($increment = $dbcfg->pompoSettings['dayDeliveryIncrement'][$expeditionDate->format('N')] ?? 0) {
            return (int) $increment;
        }

        return 0;
    }

    /**
     * Odecte od incrementu nepracovni dny, aby kdyz se zavola DateUtil::calcWorkingDays nebylo datum o ty nepracovni dny posunuty.
     *
     * TODO: Zpusobilo to problemy s datumem doruceni pred svatkama (4.7.2023). Upravovalo se to pred Vanocema, takze tam to taky delalo nejakej problem. Nasimulovat, projit a vyresit!
     */
    private function getDateIncrementWithoutWorkdays(int $increment): int
    {
        if ($increment <= 0) {
            return $increment;
        }

        foreach (range(1, $increment) as $i) {
            $date = (new \DateTime())->add(new \DateInterval('P'.$i.'D'));
            if (!DateUtil::isWorkday($date)) {
                $increment--;
            }
        }

        return $increment;
    }

    /**
     * Vraci pocet dnu do dalsiho objednavaciho dne od dodavatele.
     */
    private function getSupplierOrderIncrement(int $supplierId = self::SUPPLIER_ID): int
    {
        static $supplierIncrement = [];

        if (!($supplierIncrement[$supplierId] ?? false)) {
            $supplier = sqlQueryBuilder()
                ->select('*')
                ->from('suppliers')
                ->where(Operator::equals(['id' => $supplierId]))
                ->execute()->fetchAssociative();

            $supplier['data'] = json_decode($supplier['data'] ?? '', true) ?? [];

            $today = new \DateTime();

            $orderHours = array_filter($supplier['data']['order_hours'] ?? []);

            $increment = null;
            // zkontrolovat, zda se bude dneska jeste objednavat od dodavatele
            if ($todayOrderHour = $orderHours[$today->format('N')] ?? null) {
                [$hour, $minute] = explode(':', $todayOrderHour);
                $todayOrderDate = (new \DateTime())->setTime((int) $hour, (int) $minute);
                // stiham objednat jeste dneska
                if ($today < $todayOrderDate) {
                    $increment = 0;
                }
            }

            if ($increment === null) {
                // najit nejblizsi den, kdy se delaji objednavky dodavatelovi
                $increment = $this->recursivelyGetClosestDeliveryDateIncrement($supplier['data']['order_hours'] ?? []);
            }

            $supplierIncrement[$supplierId] = $increment;
        }

        return $supplierIncrement[$supplierId];
    }

    /**
     * Rekurzivne prochazi objednaci casy nastavene u dodavatele, aby se nasel nejblizsi den, kdy se od dodavatele bude objednavat.
     */
    private function recursivelyGetClosestDeliveryDateIncrement(array $orderHours, int $increment = 0, bool $deep = false): int
    {
        $today = new \DateTime();

        $found = false;
        foreach ($orderHours as $day => $hour) {
            if (!$deep && $day <= $today->format('N')) {
                continue;
            }

            // if is weekday and hour is empty, skip
            // weekday should not be added to increment, because we are adding only working days
            if (in_array($day, [6, 7]) && empty($hour)) {
                continue;
            }

            $increment++;

            if (!empty($hour)) {
                $found = true;
                break;
            }
        }

        if (!$found) {
            if ($deep) {
                return 0;
            }

            $increment = $this->recursivelyGetClosestDeliveryDateIncrement($orderHours, $increment, true);
        }

        return $increment;
    }

    /**
     * Nafetchuje k produktum increment podle zaznamu v products of suppliers - kazdy produkt muze byt totiz delivery increment jeste povyseny.
     */
    public function fetchProductsSupplierDeliveryDateIncrement(ProductCollection $products): ProductCollection
    {
        if ($products->count() === 0) {
            return $products;
        }

        if (isset($products->_pompoSupplierDeliveryDateIncrementFetched)) {
            return $products;
        }

        $qb = sqlQueryBuilder()
            ->select('pos.id_supplier, pos.id_product, pos.id_variation, MIN(COALESCE(pos.note, COALESCE(IF(JSON_VALUE(data, \'$.delivery_time\')=\'\', 4, JSON_VALUE(data, \'$.delivery_time\')), 4))) as note')
            ->from('products_of_suppliers', 'pos')
            ->join('pos', 'suppliers', 's', 's.id = pos.id_supplier')
            ->andWhere('pos.in_store > 0')
            ->groupBy('pos.id_product, pos.id_variation');

        $specs = [];
        foreach ($products as $key => $product) {
            $id = explode('/', (string) $key);
            $productId = (int) $id[0];
            $variationId = null;
            if (!empty($id[1])) {
                $variationId = (int) $id[1];
            }

            $specs[] = Operator::equalsNullable(['id_product' => $productId, 'id_variation' => $variationId]);
        }

        $qb->andWhere(Operator::orX($specs));

        foreach ($qb->execute() as $item) {
            $key = $item['id_product'];
            if ($item['id_variation']) {
                $key .= '/'.$item['id_variation'];
            }

            if ($products[$key] ?? false) {
                $products[$key]->_pompoSupplierId = $item['id_supplier'] ?? null;
                $products[$key]->supplierDeliveryDateIncrement = !empty($item['note']) ? (int) $item['note'] : null;
            }
        }

        $products->_pompoSupplierDeliveryDateIncrementFetched = true;

        return $products;
    }

    /**
     * Nastaví produktům stitek podle podmínek.
     *
     * Bezva cena - DMOC sleva >= 16
     * Cenový hit - DMOC sleva >= 36
     */
    public function generateProductDiscountLabels(): void
    {
        // Bezva cena
        $labelIdBC = $this->labelUtil->getLabelIdByCode('BC');
        // Cenový hit
        $labelIdCH = $this->labelUtil->getLabelIdByCode('CH');

        if (!$labelIdBC || !$labelIdCH) {
            return;
        }

        $productList = clone $this->productList;
        $productList->fetchProductOfSuppliersInStore();
        $productList->applyDefaultFilterParams();
        $productList->addResultModifiers(fn (ProductCollection $products) => $this->fetchAdditionalPrices($products));

        $batchSize = 500;

        $updateData = [
            $labelIdBC => [],
            $labelIdCH => [],
        ];

        // projdu produkty abych nasel ty, kterym potrebuju nastavit kampan
        $iterationLimit = $productList->getProductsCount() / $batchSize;
        for ($i = 0; $i < $iterationLimit; $i++) {
            $productList->limit($batchSize, $i * $batchSize);

            foreach ($productList->getProducts() as $product) {
                if (!isset($product->dmocDiscount)) {
                    continue;
                }

                if (!$product->dmocDiscount->isPositive()) {
                    continue;
                }

                $discount = $product->dmocDiscount->asFloat();

                // Cenovy hit
                if ($discount >= 36) {
                    $updateData[$labelIdCH][] = $product->id;
                // Bezva cena
                } elseif ($discount >= 16) {
                    $updateData[$labelIdBC][] = $product->id;
                }
            }
        }

        foreach ($updateData as $labelId => $data) {
            // smazu vygenerovane stitky od produktu
            sqlQueryBuilder()
                ->delete('product_labels_relation')
                ->where(Operator::equals(['id_label' => $labelId]))
                ->execute();

            $baseInsertQb = sqlQueryBuilder()
                ->insert('product_labels_relation')
                ->onDuplicateKeyUpdate(['id_label', 'id_product']);

            // nastavim stitek k produktum
            foreach (array_chunk($data, 500) as $products) {
                $qb = clone $baseInsertQb;
                foreach ($products as $productId) {
                    $qb->multiDirectValues(['id_label' => $labelId, 'id_product' => $productId, 'generated' => 1]);
                }
                $qb->execute();
            }
        }
    }

    public function updateProductsVIPPriceList(): void
    {
        $selectQb = sqlQueryBuilder()
            ->select($this->configuration->getVIPPriceListId(), 'p.id', 'ROUND(p.price_buy * 1.05, 4) as vip_price')
            ->from('products', 'p')
            ->where('price_buy IS NOT NULL AND price_buy > 0');

        sqlQuery("INSERT INTO pricelists_products (id_pricelist, id_product, price)
            {$selectQb->getSQL()}
            ON DUPLICATE KEY UPDATE price = VALUES(price);");
    }

    /**
     * Nastaví produktům flagy podle podmínek.
     *
     * Bezva cena - DMOC sleva >= 16
     * Cenový hit - DMOC sleva >= 36
     *
     * @depracated Delete me when labels are fully used instead of campaigns
     */
    public function generateProductDiscountFlags(): void
    {
        $campaigns = getCampaigns();
        if (!isset($campaigns['BC']) || !isset($campaigns['CH'])) {
            return;
        }

        $productList = clone $this->productList;
        $productList->fetchProductOfSuppliersInStore();
        $productList->applyDefaultFilterParams();
        $productList->addResultModifiers(fn (ProductCollection $products) => $this->fetchAdditionalPrices($products));

        $batchSize = 500;

        $updateData = [
            'BC' => [],
            'CH' => [],
        ];

        // projdu produkty abych nasel ty, kterym potrebuju nastavit kampan
        $iterationLimit = $productList->getProductsCount() / $batchSize;
        for ($i = 0; $i < $iterationLimit; $i++) {
            $productList->limit($batchSize, $i * $batchSize);

            foreach ($productList->getProducts() as $product) {
                if (!isset($product->dmocDiscount)) {
                    continue;
                }

                if (!$product->dmocDiscount->isPositive()) {
                    continue;
                }

                $discount = $product->dmocDiscount->asFloat();

                // Cenovy hit
                if ($discount >= 36) {
                    $updateData['CH'][] = $product->id;
                // Bezva cena
                } elseif ($discount >= 16) {
                    $updateData['BC'][] = $product->id;
                }
            }
        }

        foreach ($updateData as $campaign => $data) {
            // smazu kampan od produktu
            sqlQueryBuilder()
                ->update('products')
                ->set('campaign', 'REMOVE_FROM_SET(:campaign, campaign)')
                ->where(Operator::findInSet([$campaign], 'campaign'))
                ->setParameter('campaign', $campaign)
                ->execute();

            // nastavim kampan k produktum
            foreach (array_chunk($data, 500) as $products) {
                sqlQueryBuilder()
                    ->update('products')
                    ->set('campaign', 'ADD_TO_SET(:campaign, campaign)')
                    ->where(Operator::inIntArray(array_values($products), 'id'))
                    ->setParameter('campaign', $campaign)
                    ->execute();
            }
        }
    }

    /**
     * Vrací prodejce indexované podle ID skladu.
     */
    private function getSellersByStoreId(): array
    {
        return Mapping::mapKeys(
            Contexts::get(SellerContext::class)->getSupported(),
            function ($k, $v) {
                return [$v['id_store'], $v];
            }
        );
    }

    /**
     * Vrací `ProductCollection` podle zadaných ID produktů.
     */
    private function getProducts(array $products): ProductCollection
    {
        static $productsCache = [];

        $cacheKey = md5(serialize($products));

        if (!($productsCache[$cacheKey] ?? false)) {
            $productList = clone $this->productList;
            $productList->setVariationsAsResult(true);
            $collection = $productList->andSpec(Product::productsAndVariationsIds($products))
                ->getProducts();

            $collection->fetchProductOfSuppliersInStore();

            $productsCache[$cacheKey] = $collection;
        }

        return $productsCache[$cacheKey];
    }
}
