<?php

declare(strict_types=1);

namespace External\PompoBundle\Util\Ordering;

use External\PompoBundle\DataGo\Util\DataGoApi;
use External\PompoBundle\DRS\Util\DRSApi;
use External\PompoBundle\Exception\PompoCartException;
use External\PompoBundle\Util\Configuration;
use External\PompoBundle\Util\PompoUtil;
use External\PompoBundle\Util\ProductUtil;
use KupShop\CatalogBundle\ProductList\ProductCollection;
use KupShop\KupShopBundle\Context\CurrencyContext;
use KupShop\KupShopBundle\Util\ArrayUtil;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\KupShopBundle\Util\DateUtil;
use KupShop\KupShopBundle\Util\Functional\Mapping;
use KupShop\OrderingBundle\Entity\Purchase\ProductPurchaseItem;
use KupShop\OrderingBundle\Entity\Purchase\PurchaseState;
use KupShop\OrderingBundle\Exception\DeliveryException;
use KupShop\OrderingBundle\Util\Order\OrderItemInfo;
use KupShop\OrderingBundle\Util\Order\OrderUtil;
use KupShop\SellerBundle\Context\SellerContext;
use KupShop\SellerBundle\Utils\SellerUtil;
use Query\Operator;

class OrderingUtil
{
    public const CHARGES_HANDLING_FEE = [1, 2];

    private ProductUtil $productUtil;
    private Configuration $configuration;
    private OrderUtil $orderUtil;
    private OrderItemInfo $orderItemInfo;
    private PompoUtil $pompoUtil;
    private DRSApi $drsApi;
    private DataGoApi $dataGoApi;
    private ?SellerUtil $sellerUtil;

    public function __construct(
        ProductUtil $productUtil,
        Configuration $configuration,
        OrderUtil $orderUtil,
        OrderItemInfo $orderItemInfo,
        PompoUtil $pompoUtil,
        DRSApi $drsApi,
        DataGoApi $dataGoApi,
        ?SellerUtil $sellerUtil,
    ) {
        $this->productUtil = $productUtil;
        $this->configuration = $configuration;
        $this->orderUtil = $orderUtil;
        $this->orderItemInfo = $orderItemInfo;
        $this->pompoUtil = $pompoUtil;
        $this->drsApi = $drsApi;
        $this->dataGoApi = $dataGoApi;
        $this->sellerUtil = $sellerUtil;
    }

    /**
     * Rozdělí objednávku typu ORDER_TRANSPORT_RESERVATION na dvě objednávky. Jedna objednávka bude čistě s dostupnýma produktama
     * na prodejně a druhá objednávka bude obsahovat produkty, které jsou dostupné pouzen a centrálním skladu a je potřeba je
     * tedy převézt na prodejnu.
     */
    public function orderSplitTransportReservation(\Order $order): void
    {
        // rozdelujeme jen typ ORDER_TRANSPORT_RESERVATION
        if ($this->getOrderType($order) !== OrderType::ORDER_TRANSPORT_RESERVATION) {
            return;
        }

        // cely to delam v transakci
        sqlGetConnection()->transactional(function () use ($order) {
            // force-loadnu si data, protoze eventy tam mohli neco pridat a bez force-loadu bych to tam nemel
            $orderData = $order->getDataAll(true);
            $sellerId = empty($orderData['sellerId']) ? null : (int) $orderData['sellerId'];

            $items = $this->getPurchaseStateProducts($order->getPurchaseState(), $sellerId, false);
            $itemsGrouped = $this->getPurchaseStateProductsGrouped($order->getPurchaseState());
            // prvne se podivam, zda je co rozdelovat, abych nevytvoril prazdnou rezervacni objednavku
            $reservationItems = false;
            foreach ($items as $item) {
                if (($item->getProduct()->inStoreSeller ?? 0) > 0) {
                    $reservationItems = true;
                    break;
                }
            }

            if (!$reservationItems) {
                return;
            }

            // zduplikuju objednavku - tohle bude objednavka s cistou rezervaci
            $reservationOrder = $this->orderUtil->copyOrder($order->id);
            $reservationOrder->status_previous = -1;
            $reservationOrder->status = -1;
            $reservationOrder->setData('orderType', OrderType::ORDER_RESERVATION);
            $reservationOrder->setData('orderPartId', 2);

            $deliveryData = array_merge($orderData['delivery_data'] ?? [], ['seller_id' => $sellerId]);
            // TODO: mozna bude potreba pridat vetsi logiku
            $deliveryData['deliveryDate'] = (new \DateTime())->format('Y-m-d');

            // insertnu vazbu objednavky na prodejnu k rezervacni objednavce
            if ($sellerId) {
                sqlQueryBuilder()->insert('order_sellers')
                    ->directValues([
                        'id_order' => $reservationOrder->id,
                        'id_seller' => $sellerId,
                    ])
                    ->onDuplicateKeyUpdate(['id_order', 'id_seller'])
                    ->execute();
            }

            $reservationOrder->setData('delivery_data', $deliveryData);
            // Odeberu flag "Rezervace s prepravou" po zkopirovani z puvodni objednavky
            $this->orderUtil->removeFlag($reservationOrder, 'RT');
            // pridam flag "Rezervace"
            $this->orderUtil->addFlag($reservationOrder, 'RZ');
            // pridam flag "Vznikla rozdelenim"
            $this->orderUtil->addFlag($reservationOrder, 'VR');

            foreach ($items as $item) {
                $key = $this->getPurchaseItemKey($item);
                $itemGrouped = $itemsGrouped[$key] ?? [];
                $itemGrouped->inStoreSeller = ($itemGrouped->inStoreSeller ?? ($item->getProduct()->inStoreSeller ?? 0));
                // pokud to neni skladem na prodejne, tak to zustava v objednavce
                if ($itemGrouped->inStoreSeller <= 0) {
                    continue;
                }

                // pocet kusu, ktere muzu zarezervovat
                $reservationPieces = min($itemGrouped->inStoreSeller, $item->getPieces());
                $newPieces = $item->getPieces() - $reservationPieces;

                // zduplikuju item do rezervace s novym poctem kusu
                $this->copyOrderItem($reservationOrder, (int) $item->getId(), (int) $reservationPieces);
                $itemGrouped->inStoreSeller -= $reservationPieces;

                // u aktualni objednavky odeberu pocet kusu, ktere sli do rezervace
                $order->updateItem($item->getId(), $newPieces);
                // smazu item z objednavky, pokud slo vsechno do rezervace
                if ($newPieces <= 0) {
                    sqlQueryBuilder()
                        ->delete('order_items')
                        ->where(Operator::equals(['id' => $item->getId()]))
                        ->execute();
                }
            }

            // nastavim na objednavku ID objednavky s rezervaci - je to hlavne kvuli FE, abychom pak mohli na dekovacce zobrazit odkazy na obe objednavky
            $order->setData('reservationOrderId', $reservationOrder->id);
            $order->setData('orderPartId', 1);

            // resetuju purchase state objednavky
            $order->setPurchaseState(null);

            // recalculate na objednavky
            $reservationOrder->recalculate();
            $order->recalculate();

            // resetnu itemy na objednavce, aby se znovu loadnuly, kdyz by si o ne nekdo rekl a bylo to tam spravne
            $order->items = [];

            // Odeslat e-mail k rezervacni objednavce
            $reservationOrder->changeStatus(0);

            // Zaloguju k objednavce, ze z ni byla vytvorena rezervacni objednavka
            $order->logHistory(
                sprintf('Z této objednávka byla vytvořena rezervační objednávka číslo <a href="javascript:nw(\'orders\', \'%s\')">%s</a>', $reservationOrder->id, $reservationOrder->order_no)
            );

            // Zaloguju k rezervacni objednavce info o tom, ze vznikla rozdelenim
            $reservationOrder->logHistory(
                sprintf('Rezervační objednávka, která vznikla rozdělením z objednávky číslo <a href="javascript:nw(\'orders\', \'%s\')">%s</a>', $order->id, $order->order_no)
            );
        });
    }

    /**
     * Vrátí typ objednávky - jeden z typů definovaných v classe OrderType.
     */
    public function getOrderType(\Order $order): string
    {
        // Objednavka uz na sobe orderType ma ulozeny
        if ($orderType = $order->getData('orderType')) {
            return $orderType;
        }

        // objednávka je přes přepravce
        if (!$this->isOrderInPerson($order)) {
            return OrderType::ORDER_TRANSPORT;
        }

        // kouknu, jestli ma objednavka manipulacni poplatek
        // nebo je to osobni odber s centralnim skladem
        if ($this->isTransportChargeInOrder($order) || $this->isOrderInPersonWithMainStore($order)) {
            return OrderType::ORDER_TRANSPORT_RESERVATION;
        }

        return OrderType::ORDER_RESERVATION;
    }

    /**
     * Lze pouzit pouze pokud vim, ze je doprava osobni odber. Protoze s PurchaseStatu nepoznam o jakou dopravu se jedna.
     *
     * Vrati to teda ORDER_TRANSPORT_RESERVATION nebo ORDER_RESERVATION
     */
    public function getPurchaseStateOrderType(PurchaseState $purchaseState, int $sellerId): string
    {
        $seller = $this->sellerUtil->getSeller($sellerId);

        $notInSellerStore = false;
        foreach ($this->getPurchaseStateProducts($purchaseState, $sellerId) as $item) {
            // pokud neni v dostatecnem skladu na prodejne, tak uz to nemuze byt cista rezervace
            // nebo pokud se jedna o prodejnu, ktera ma nastaveny sklad na hlavni sklad - v takovem pripade to taky nesmi byt cista rezervace
            if (($item->getProduct()->inStoreSeller ?? 0) < $item->getPieces() || $seller['id_store'] === $this->configuration->getMainStoreId()) {
                $notInSellerStore = true;
                break;
            }
        }

        if ($notInSellerStore) {
            return OrderType::ORDER_TRANSPORT_RESERVATION;
        }

        return OrderType::ORDER_RESERVATION;
    }

    /**
     * Vrací informaci o tom, zda pro PurchaseState existuje prodejna, na kterou je možné provést závoz.
     */
    public function hasProductWithInPersonTransferDelivery(PurchaseState $purchaseState): bool
    {
        if (!$this->configuration->isPompo()) {
            return false;
        }

        $transferDeliverySupported = false;

        $sellerContext = Contexts::get(SellerContext::class);
        $sellers = $sellerContext->getSupported();

        $this->fetchSellersCartInfo(
            $purchaseState,
            $sellers
        );

        foreach ($sellers as $seller) {
            if ($seller['availability'] === ProductAvailability::SELLER_IN_STORE_TRANSFER) {
                $transferDeliverySupported = true;
                break;
            }
        }

        return $transferDeliverySupported;
    }

    /**
     * Fetchne skladovou dostupnost k jednotlivym produktum v PurchaseStatu. Například na druhém kroku dopravy, kde
     * zobrazujeme produkty s dostupností.
     */
    public function fetchPurchaseStateProductsAvailability(PurchaseState $purchaseState, ?int $sellerId = null, ?string $pickType = null): void
    {
        $productsInfo = $this->getPurchaseStateProducts($purchaseState, $sellerId);
        $giftsInfo = $this->getPurchaseStateGifts($purchaseState, $sellerId);

        $getInfoItem = function (ProductPurchaseItem $item) use ($productsInfo, $giftsInfo) {
            $key = $this->getPurchaseItemKey($item);

            if (!($infoItem = $productsInfo[$key] ?? null)) {
                if (!($infoItem = $giftsInfo[$key] ?? null)) {
                    return null;
                }
            }

            return $infoItem;
        };

        // zmerguju si produkty se slevama, abych prosel vsechno naraz
        $items = array_merge($purchaseState->getProducts(), $purchaseState->getDiscounts());
        // doplnim jeste darky od produktu z additional items
        foreach ($purchaseState->getProducts() as $item) {
            foreach ($item->getAdditionalItems() as $additionalItem) {
                $items[] = $additionalItem;
            }
        }

        foreach ($items as $item) {
            // pokud to neni ProductPurchaseItem, tak me to nezajima
            if (!($item instanceof ProductPurchaseItem)) {
                continue;
            }

            if (!($infoItem = $getInfoItem($item))) {
                continue;
            }

            $product = $infoItem->getProduct();
            $pieces = (float) $infoItem->getPieces();

            $totalMainStore = ($product->inStoreMain ?? 0) + ($product->inStoreSupplier ?? 0);
            $totalSellerStore = ($product->inStoreMain ?? 0) + ($product->inStoreSupplier ?? 0) + ($product->inStoreSeller ?? 0);

            $productAvailability = ProductAvailability::NOT_IN_STORE;
            // Pokud je na hlavnim skladu dostatek mnozstvi
            if (($product->inStoreMain ?? 0) >= $pieces) {
                $productAvailability = ProductAvailability::IN_STORE;
            } elseif (($product->inStoreSupplier ?? 0) >= ($pieces - ($product->inStoreMain ?? 0))) {
                $productAvailability = ProductAvailability::IN_STORE_SUPPLIER;
            }

            if ($sellerId) {
                if ($pickType === 'complete') {
                    $productAvailability = $productAvailability > ProductAvailability::NOT_IN_STORE ? ProductAvailability::SELLER_IN_STORE_PARTIALLY : ProductAvailability::NOT_IN_STORE;
                }

                if ($pickType !== 'complete') {
                    $productAvailability = $this->getSellerAvailability(
                        $product->inStoreSeller ?? 0,
                        ($product->inStoreMain ?? 0) + ($product->inStoreSupplier ?? 0),
                        $pieces
                    );
                }
            }

            // pridam notu, abych v kosiku byl schopny zobrazit primo u produktu stav skladu (napr. na kroku s dopravou)
            $item->addNote('availability', $productAvailability);

            if ($sellerId && $totalSellerStore < $pieces) {
                // pridam notu, abych v kosiku byl schopny zobrazit primo u produktu stav skladu (napr. na kroku s dopravou)
                $item->addNote('availability', ProductAvailability::NOT_IN_STORE);
            }

            if (!$sellerId && $totalMainStore < $pieces) {
                // pridam notu, abych v kosiku byl schopny zobrazit primo u produktu stav skladu (napr. na kroku s dopravou)
                $item->addNote('availability', ProductAvailability::NOT_IN_STORE);
            }
        }
    }

    /**
     * Kontrola dostupnosti pro každou dopravu. Kontrola se provádí pro každou dopravu, abych dokázal správně zadisablovat
     * dopravy, přes které nejde objednat.
     *
     * Kontroluje dostupnost produktu na prodejnach vs centralni sklad pro osobni odbery a pro dopravce.
     *
     * 1) Na prodejnu mohu objednat bud z centralniho skladu (zavoz na prodejnu) nebo muzu rezervovat zbozi na prodejne, ale nemohu
     *    objednat zbozi z jine prodejny.
     * 2) Pokud objednavam pres dopravce, tak musi byt produkt skladem na centralnim sklade nebo u dodavatele, ale nemohu
     *    objednat produkt z prodejny.
     */
    public function validatePurchaseStateByDelivery(\Delivery $delivery, PurchaseState $purchaseState): void
    {
        $selectedDeliveryType = $purchaseState->getDeliveryType();

        // pokud se jedna o osobni odber
        if ($delivery instanceof \OdberNaProdejne) {
            if ($delivery->getPointId()) {
                $sellerId = (int) $delivery->getPointId();

                $seller = $this->sellerUtil->getSeller($sellerId);
                // zkontroluju, ze se na daneho sellera da objednavat - muzou prodejnu uzvarit napr. kvuli inventure
                if (!$seller || ($seller['data']['orders_disabled'] ?? 'N') === 'Y') {
                    throw new DeliveryException(
                        translate('seller_sellerOrdersDisabled', 'pompo'),
                        translate('seller_sellerOrdersDisabled', 'pompo'),
                        DeliveryException::ERROR_SOFT
                    );
                }

                // zvaliduju produkty v PurchaseState
                foreach ($this->getPurchaseStateProducts($purchaseState, $sellerId) as $item) {
                    $product = $item->getProduct();
                    $pieces = (float) $item->getPieces();

                    // objednavam na prodejnu a nemam to skladem na centrala nebo na vybrane prodejne - je to nejspis skladem pouze na jine prodejne
                    if ($seller['id_store'] != $this->configuration->getMainStoreId()) {
                        $totalSellerStore = ($product->inStoreMain ?? 0) + ($product->inStoreSupplier ?? 0) + max($product->inStoreSeller ?? 0, 0);
                    } else {
                        $totalSellerStore = ($product->inStoreMain ?? 0) + ($product->inStoreSupplier ?? 0);
                    }
                    // musim mit vybranou dopravu "Osobni odber" a prodejnu, abych mohl zacit spoustet validaci
                    if ($selectedDeliveryType && $sellerId) {
                        // kontrola restrikci, pokud mam dostatecny sklad, ale neni dostatek kusu na prodejne a bude se muset zavazet z centralniho skladu
                        // a teprve v takovou chvili muzu spustit kontrolu restrikci
                        if ($totalSellerStore >= $pieces && ($product->inStoreSeller ?? 0) < $pieces) {
                            try {
                                // povolit a zkontrolovat restrikce
                                $delivery->restrictionsEnabled = true;
                                $delivery->checkRestrictions($purchaseState);
                                $delivery->restrictionsEnabled = false;
                            } catch (DeliveryException $e) {
                                $delivery->restrictionsEnabled = false;
                                // re-throw as soft error
                                throw new DeliveryException(
                                    $e->getMessage(),
                                    $e->getShortMessage(),
                                    DeliveryException::ERROR_SOFT
                                );
                            }
                        }

                        // kontrola dostupnosti
                        if ($totalSellerStore < $pieces) {
                            // vyhodim chybu, ale doprava "Osobni odber" neni zadisablovana, protoze si muzu chtit zmenit na jinou prodejnu
                            throw new DeliveryException(
                                // Produkt "%s" není na vybrané prodejně dostupný.
                                sprintf(
                                    translate('seller_productNotInStore', 'pompo'),
                                    trim($item->getName()),
                                    $totalSellerStore
                                )
                            );
                        }
                    }
                }
            }

            return;
        }

        // validace PurchaseStatu pro ostatni dopravce
        static $products;

        if (!$products) {
            // pro dopravce mi staci nacist data k PurchaseStatu jen jednou, protoze tam by nemel byt zadny rozdil
            $products = $this->getPurchaseStateProducts($purchaseState);
        }

        // zvaliduju produkty v PurchaseState
        foreach ($products as $item) {
            $product = $item->getProduct();
            $pieces = (float) $item->getPieces();

            // kontroluju jednotlive dopravce dopravce, takze skladovost je hlavni sklad + sklad dodavatele
            $totalMainStore = ($product->inStoreMain ?? 0) + ($product->inStoreSupplier ?? 0);
            if ($totalMainStore < $pieces) {
                // pokud nemam dostatecne mnozstvi skladem, tak vyhazuju chybu, ktera disabluje i dopravy, aby nesla vybrat
                $deliveryException = new DeliveryException(
                    // Produkt "%s" není možné objednat přes dopravce, protože je skladem pouze na vybraných prodejnách.
                    sprintf(
                        translate('productNotInMainStore', 'pompo'),
                        trim($item->getName()),
                        $totalMainStore
                    ),
                    // %s - na centralnim skladu je pouze %s ks
                    sprintf(
                        translate('productNotInMainStoreShort', 'pompo'),
                        trim($item->getName()),
                        $totalMainStore
                    ),
                    DeliveryException::ERROR_DELIVERY_DISABLED
                );

                // pokud je dana doprava vybrana, tak vyhodim chybu aby se zobrazila i cervena hlaska nad kosikem
                if ($selectedDeliveryType && $selectedDeliveryType->id_delivery == $delivery->id) {
                    throw $deliveryException;
                }

                // pokud doprava vybrana neni, tak jen nastavim chybu k doprave, aby se doprava disablovala a zobrazila u ni hlaska
                $delivery->exception = $deliveryException;
            }
        }
    }

    /**
     * Nafetchuje skladovou dostupnost k prodejcum - pouziva se to v kosiku, pri vybirani prodejny pro vyzvednuti.
     *
     * Zaroven to ke kazdemu prodejci prida deliveryDateIncrement, kterej se v kosiku pricte k predpokladanemu datu dorucenu
     *
     * $availability - viz. \External\PompoBundle\Util\Ordering\ProductAvailability
     */
    public function fetchSellersCartInfo(PurchaseState $purchaseState, array &$sellers): void
    {
        // potrebuju si nafetchovat pripadny increment u dodavatele (kdyby jediny misto, kde je skladem byl dodavatel)
        $fetchProductSupplierIncrements = function (ProductCollection $products) {
            $this->productUtil->fetchProductsSupplierDeliveryDateIncrement($products);
        };

        $giftProducts = $this->getPurchaseStateGifts($purchaseState, null);

        $productQuantityMainStore = [];
        $productQuantityByStore = [];
        // ulozim si skladova data do pole, aby se mi s tim lepe pracovalo
        foreach ($this->getPurchaseStateProducts($purchaseState, null, true, $fetchProductSupplierIncrements) as $item) {
            $product = $item->getProduct();

            // Skladovost hlavni sklad + dodavatel
            $productQuantityMainStore[$item->getId()] = ($product->inStoreMain ?? 0) + ($product->inStoreSupplier ?? 0);
            // Skladovost dodavatele
            $productQuantitySupplier[$item->getId()] = $product->inStoreSupplier ?? 0;

            // Skladovost na jednotlivych prodejnach
            foreach ($product->storesInStore ?? [] as $storeId => $store) {
                $productQuantityByStore[$storeId][$item->getId()] = $store['in_store'];
            }
        }

        // projdu vsechny prodejny, abych k nim nacetl dostupnost
        foreach ($sellers as &$seller) {
            $availability = null;
            $deliveryDateIncrement = 0;
            $completePickupAvailable = true;

            // nactu si dostupnost darku v kosiku - zajima me jestli je na prodejne dostupny, nebo musi jit z centralniho skladu
            $giftsSellerAvailability = null;
            foreach ($giftProducts as $giftProduct) {
                if (($giftProduct->getProduct()->storesInStore[$seller['id_store']]['in_store'] ?? 0) > 0) {
                    $giftsSellerAvailability = $giftsSellerAvailability === null ? 1 : min(1, $giftsSellerAvailability);
                } else {
                    $giftsSellerAvailability = $giftsSellerAvailability === null ? 0 : min(0, $giftsSellerAvailability);
                }
            }

            // projdu produkty v kosiku
            foreach ($this->getPurchaseStateProductsGrouped($purchaseState) as $item) {
                $mainStoreQuantity = (float) ($productQuantityMainStore[$item->getId()] ?? 0);
                $sellerStoreQuantity = (float) ($productQuantityByStore[$seller['id_store']][$item->getId()] ?? 0);

                $productAvailability = $this->getSellerAvailability(
                    $sellerStoreQuantity,
                    $mainStoreQuantity,
                    (float) $item->getPieces()
                );

                // pokud to ma byt zavoz na prodejnu, tak musim udelat kontrolu restrikci
                if ($productAvailability === ProductAvailability::SELLER_IN_STORE_TRANSFER) {
                    // kontrola restrikci, pokud mam dostatecny sklad, ale neni dostatek kusu na prodejne a bude se muset zavazet z centralniho skladu
                    if (($mainStoreQuantity + $sellerStoreQuantity) > $item->getPieces() && $sellerStoreQuantity < $item->getPieces()) {
                        // najdu si dopravu "OdberNaProdejne"
                        $inPersonDeliveries = array_filter(\Delivery::getAll(), fn ($x) => $x instanceof \OdberNaProdejne);
                        if (!empty($inPersonDeliveries) && ($inPersonDelivery = reset($inPersonDeliveries))) {
                            try {
                                $inPersonDelivery->restrictionsEnabled = true;
                                $inPersonDelivery->checkRestrictions($purchaseState);
                                $inPersonDelivery->restrictionsEnabled = false;
                            } catch (DeliveryException $e) {
                                // pokud restrikce nedovoluji zavoz na prodejnu, tak prodejnu oznacim jako, ze to tam neni dostupne
                                $productAvailability = ProductAvailability::NOT_IN_STORE;
                                $inPersonDelivery->restrictionsEnabled = false;
                            }
                        }
                    }
                }

                // Ukladam si, zda je mozne zavest na prodejnu jako kompletni zasilku - v pripade, ze na prodejne neni vse skladem
                // tak muzu vybirat mezi "vyzvednout po castech" nebo "vyzvednout kompletni", ale k tomu, aby to slo vyzvednout komplet
                // je potreba, aby vsechno bylo skladem na centralnim skladu, odkud se ta kompletni zasilka na prodejnu zaveze
                if (($productQuantityMainStore[$item->getId()] ?? 0) < $item->getPieces()) {
                    $completePickupAvailable = false;
                }

                // Pokud je produkt pouze na centralnim skladu, ale neco predtim uz bylo i na prodejne, tak chci zobrazovat "castecne skladem"
                if ($availability >= ProductAvailability::SELLER_IN_STORE_PARTIALLY && $productAvailability === ProductAvailability::SELLER_IN_STORE_TRANSFER) {
                    $productAvailability = ProductAvailability::SELLER_IN_STORE_PARTIALLY;
                }

                // Pokud je produkt na prodejne, ale neco predtim bylo pouze na centralnim skladu, tak chci zobrazovat "castecne skladem"
                if ($availability === ProductAvailability::SELLER_IN_STORE_TRANSFER && $productAvailability >= ProductAvailability::SELLER_IN_STORE_PARTIALLY) {
                    $availability = ProductAvailability::SELLER_IN_STORE_PARTIALLY;
                }

                $availability = $availability === null ? $productAvailability : min($availability, $productAvailability);

                // jakmile nema vsechny kusy produktu na prodejne, a nemam ani dostatek na centralnim skladu, tak zacinam resit deliveryDateIncrement ze skladu dodavatele
                $inStoreMainWithoutSupplier = ($productQuantityMainStore[$item->getId()] ?? 0) - ($productQuantitySupplier[$item->getId()] ?? 0);
                if ($availability < ProductAvailability::IN_STORE && $inStoreMainWithoutSupplier < $item->getPieces() && ($productQuantitySupplier[$item->getId()] ?? 0) > 0) {
                    $deliveryDateIncrement = max($deliveryDateIncrement, $item->getProduct()->supplierDeliveryDateIncrement ?? 0);
                }
            }

            // pokud mam v kosiku darky a nektery z darku neni dostupny, tak se osobni odber musi tvarit jako zavoz
            if ($giftsSellerAvailability === 0) {
                $availability = ProductAvailability::SELLER_IN_STORE_TRANSFER;
            }

            // ulozim dostupnost na prodejce
            // viz. viz. \External\PompoBundle\Util\Ordering\ProductAvailability
            $seller['availability'] = $availability;

            // ulozim si, zda je mozne vyzvednout jako kompletni zasilku v pripade, ze je zbozi na prodejne castecne skladem
            $seller['completePickupAvailable'] = $completePickupAvailable;

            // jeste si potrebuju spocitat date increment pro prodejnu a pak navysit o pripadny deliveryDateIncrement
            $seller['deliveryDate'] = DateUtil::calcWorkingDays($deliveryDateIncrement, $this->getSellerDeliveryDate($seller, $availability));
            // pokud je zbozi castecne skladem, tak v kosiku davame moznost rozdelit objednavku, aby neco slo vyzvednout ihned a neco az pozdeji
            if ($availability === ProductAvailability::SELLER_IN_STORE_PARTIALLY) {
                $seller['deliveryDatePartially'] = $this->getSellerDeliveryDate($seller, ProductAvailability::IN_STORE);
            }
        }
    }

    /**
     * Vrátí dostupnost na konkrétní prodejně.
     *
     * 0 - nedostupne (neni skladem nikde)
     * 1 - na prodejnu zavezeme (je skladem na centralnim sklade nebo u dodavatele, takze na prodejnu muzeme zavezt)
     * 2 - castecne skladem (cast je skladem na prodejne, ale cast jen na centralnim sklade nebu u dodavatele)
     * 3 - skladem (vsechno je skladem na prodejne)
     */
    public function getSellerAvailability(float $sellerInStore, float $mainInStore, float $pieces): int
    {
        $productAvailability = ProductAvailability::NOT_IN_STORE;

        // Produkt je v dostatecnem mnozstvi skladem na prodejne
        if ($sellerInStore >= $pieces) {
            $productAvailability = ProductAvailability::IN_STORE;
        // Produkt je na prodejne jen v castecnem mnozstvi
        } elseif ($sellerInStore > 0) {
            $productAvailability = ProductAvailability::SELLER_IN_STORE_PARTIALLY;
        }

        // Produkt je dostupny na centralnim skladu
        if ($mainInStore > 0) {
            $productAvailability = max($productAvailability, ProductAvailability::SELLER_IN_STORE_TRANSFER);
        }

        return $productAvailability;
    }

    /**
     * Nacte ke vsem produktum v PurchaseStatu informace okolo skladu.
     *
     * @return ProductPurchaseItem[]
     */
    public function getPurchaseStateProducts(PurchaseState $purchaseState, ?int $sellerId = null, bool $grouped = true, ?callable $customResultModifier = null): array
    {
        return $this->activateSeller(
            $sellerId,
            function () use ($purchaseState, $grouped, $customResultModifier) {
                $products = $purchaseState->createProductCollection();

                // k PurchaseStatu si nactu informace o skladu
                $this->productUtil->fetchStoreInfo($products);

                if ($customResultModifier) {
                    $customResultModifier($products);
                }

                return $grouped ? $this->getPurchaseStateProductsGrouped($purchaseState) : $purchaseState->getProducts();
            }
        );
    }

    /**
     * Vrací, zda je objednávka na osobní odběr (vyzvednutí na prodejně) nebo ne.
     */
    public function isOrderInPerson(\Order $order): bool
    {
        if ($deliveryType = $order->getDeliveryType()) {
            if ($delivery = $deliveryType->getDelivery()) {
                if ($delivery instanceof \OdberNaProdejne || $delivery->isInPerson()) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Vrací, zda je objednávka osobní odběr (vyzvednutí na prodejně) a prodejna má zároveň nastavený
     * centrální sklad jako svůj sklad.
     *
     * V tu chvíli se objednávka musí tvářit jako objednávka se závozem na prodejnu, jen tam nejsou poplatky atd..
     * z pohledu FE se to tedy tváří jako klasická rezervace, ale z pohledu backendu se to musí tvářít jako rezervace se závozem.
     */
    public function isOrderInPersonWithMainStore(\Order $order): bool
    {
        if ($this->isOrderInPerson($order)) {
            if ($sellerId = ($order->getDeliveryType()->getDelivery()->getPointId() ?: $order->getData('sellerId'))) {
                if ($seller = $this->sellerUtil->getSeller((int) $sellerId)) {
                    if ($seller['id_store'] == $this->configuration->getMainStoreId()) {
                        return true;
                    }
                }
            }
        }

        return false;
    }

    /**
     * Vrací true nebo false na základě toho, jestli je v objednávce manipulační poplatek (položka s příplatkem).
     */
    public function isTransportChargeInOrder(\Order $order): bool
    {
        foreach ($order->fetchItems() as $item) {
            if ($this->orderItemInfo->getItemType($item) === OrderItemInfo::TYPE_CHARGE) {
                if (in_array($item['note']['id_charge'] ?? null, self::CHARGES_HANDLING_FEE)) {
                    return true;
                }
            }
        }

        return false;
    }

    public function getOrderSeller(\Order $order): ?array
    {
        if (!$this->sellerUtil) {
            return null;
        }

        if ($sellerId = ($order->getData('delivery_data')['seller_id'] ?? false)) {
            if ($seller = $this->sellerUtil->getSeller((int) $sellerId)) {
                return $seller;
            }
        }

        return null;
    }

    /**
     * Vrátí dárky, které jsou vybrány v košíku.
     */
    private function getPurchaseStateGifts(PurchaseState $purchaseState, ?int $sellerId): array
    {
        $gifts = array_filter($purchaseState->getDiscounts(), fn ($x) => $x instanceof ProductPurchaseItem);
        foreach ($purchaseState->getProducts() as $item) {
            foreach ($item->getAdditionalItems() as $additionalItem) {
                if ($additionalItem instanceof ProductPurchaseItem) {
                    $gifts[] = $additionalItem;
                }
            }
        }

        if (empty($gifts)) {
            return [];
        }

        $giftProducts = Mapping::mapKeys($gifts, fn ($k, $v) => [$v->getProduct()->id, $v->getProduct()]);

        $this->activateSeller($sellerId, fn () => $this->productUtil->fetchStoreInfo(new ProductCollection($giftProducts)));

        return Mapping::mapKeys($gifts, fn ($k, $v) => [$v->getProduct()->id, $v]);
    }

    /**
     * Vrátí produkty v PurchaseStatu zgroupované podle ID produkty a ID varianty.
     *
     * Je to hlavně kvůli košíku a kontrolám skladovosti / kontrole typu objednávky, protože můžu mít v košíku dva řádky
     * stejného produktu kvůli poznámce, kde si lidé říkájí o speifickou barvu, variantu...
     */
    private function getPurchaseStateProductsGrouped(PurchaseState $purchaseState): array
    {
        $productsGrouped = [];

        foreach ($purchaseState->getProducts() as $item) {
            $key = $this->getPurchaseItemKey($item);
            // pokud uz produkt jednou mam, tak akorat aktualizuju pocet kusu
            if ($productsGrouped[$key] ?? false) {
                $itemCopy = new ProductPurchaseItem(
                    $productsGrouped[$key]->getIdProduct(),
                    $productsGrouped[$key]->getIdVariation(),
                    $productsGrouped[$key]->getPieces() + $item->getPieces(),
                    $productsGrouped[$key]->getPrice(),
                    $productsGrouped[$key]->getNote(),
                    $productsGrouped[$key]->getIdDiscount(),
                );
                $itemCopy->setId(
                    $productsGrouped[$key]->getId()
                );
                if ($productsGrouped[$key]->getProduct()) {
                    $itemCopy->setProduct($productsGrouped[$key]->getProduct());
                }
                $productsGrouped[$key] = $itemCopy;

                continue;
            }

            $productsGrouped[$key] = $item;
        }

        return $productsGrouped;
    }

    /**
     * Zkopíruje položku objednávky do jiné objednávky.
     */
    private function copyOrderItem(\Order $newOrder, int $oldItemId, ?int $newPieces = null): void
    {
        $item = sqlQueryBuilder()
            ->select('*')
            ->from('order_items')
            ->where(Operator::equals(['id' => $oldItemId]))
            ->execute()->fetchAssociative();

        if (!$item) {
            return;
        }

        unset($item['id']);

        $item['id_order'] = $newOrder->id;
        if ($newPieces) {
            $item['pieces'] = $newPieces;
            $item['pieces_reserved'] = $newPieces;
            $item['total_price'] = toDecimal($item['piece_price'])->mul(toDecimal($newPieces));
        }

        sqlQueryBuilder()
            ->insert('order_items')
            ->directValues($item)
            ->execute();
    }

    /**
     * Vrací datum doručeni pro konkrétního prodejce - používá se v košíku.
     */
    public function getSellerDeliveryDate(array $seller, int $availability): \DateTime
    {
        static $deliveryDate = null;
        static $deliveryDays = 0;

        $shipmentDate = null;
        // Pokud je vsechno skladem na prodejne, tak si to muzu vyzvednout uz dnes.
        // nebo pokud je to zavoz na prodejnu, ale prodejna ma nastaveny sklad jako Centralni sklad, tak je datum zavozu
        // v podstate ten samy den, protoze se pravdepodobne jedna o "Prodejnu", ktera se nachazi na hlavnim skladu
        if ($availability === ProductAvailability::IN_STORE) {
            // najdu nejblizsi datum, kdy si to tam budu moct vyzvednout
            $shipmentDate = $this->sellerUtil->getClosestOpenDate($seller, 3);
        }

        // loadnu si delivery date a delivery days dopravy OdberNaProdejne
        if ($deliveryDate === null) {
            $delivery = array_filter(\Delivery::getAll(), function ($d) {
                return $d instanceof \OdberNaProdejne;
            });
            $delivery = reset($delivery);

            if ($delivery) {
                $deliveryDate = $delivery->getDeliveryDate() ?: new \DateTime();
                $deliveryDays = $delivery->time_days ? (int) $delivery->time_days : 0;
            } else {
                $deliveryDate = new \DateTime();
            }
        }

        if (!$shipmentDate) {
            // najdu nejblizsi datum zavozu
            $shipmentDate = $this->getSellerClosestShipmentDate($seller, $deliveryDate);
        }

        if (!empty($seller['data']['inventory']['from']) && !empty($seller['data']['inventory']['to'])) {
            try {
                $inventoryFrom = new \DateTime($seller['data']['inventory']['from']);
                $inventoryTo = (new \DateTime($seller['data']['inventory']['to']))->setTime(23, 59, 59);
                // pokud jsem se datumem trefil zrovna do inventury, tak musim posunout datum az za inventuru
                if ($shipmentDate >= $inventoryFrom && $shipmentDate <= $inventoryTo) {
                    $shipmentDate = $this->getSellerClosestShipmentDate($seller, DateUtil::calcWorkingDays($deliveryDays, $inventoryTo->add(new \DateInterval('P1D'))));
                }
            } catch (\Throwable $e) {
            }
        }

        // Pokud budu na prodejnu zavazet a zaroven bude centralni sklad uzavren, tak musim posunout zavoz az na jiny termin
        if ($availability !== ProductAvailability::IN_STORE) {
            [$closedFrom, $closedTo] = $this->productUtil->getMainStoreClosedDates();
            if (!empty($closedFrom) && !empty($closedTo)) {
                // Pokud je centrala zrovna uzavrena, tak zavoz posouvam
                if ($shipmentDate >= $closedFrom && $shipmentDate <= $closedTo) {
                    $shipmentDate = $this->getSellerClosestShipmentDate($seller, DateUtil::calcWorkingDays($deliveryDays, $closedTo->add(new \DateInterval('P1D'))));
                }
            }
        }

        return $shipmentDate;
    }

    /**
     * Vrací nejbližší datum závozu z centrálního skladu na prodejnu.
     *
     * @param \DateTime $date - datum od kterého se má začít nejbližší datum pro závoz hledat
     */
    private function getSellerClosestShipmentDate(array $seller, \DateTime $date): \DateTime
    {
        // ulozim si cislo dne v tydnu
        $dayIndex = $date->format('N');

        $shipmentDate = clone $date;
        // pokud se zavazi ve stejny ten jako je shipmentDate, tak uz nemusim hledat jiny termin zavozu
        if ($seller['data']['shipment'][$dayIndex] ?? 0) {
            return $shipmentDate;
        }

        // najit nejblizsi den zavozu
        $closestDayIndex = null;
        foreach (range($dayIndex, 7) as $index) {
            // v dany den se nezavazi, takze pokracuju dal
            if (!($seller['data']['shipment'][$index] ?? 0)) {
                continue;
            }

            // pokud se zavasi, tak si ulozim cislo dnu v tydnu, kdy to bude
            if ($index >= $dayIndex) {
                $closestDayIndex = $index;
                break;
            }
        }

        // pokud jsem nenasel zadny termin zavozu, tak muzu byt napriklad u konce tydne a zavazi se jen zacatkem tydne
        // takze najdu prvni den zavozu v tydnu
        if ($closestDayIndex === null) {
            $shipmentDays = array_keys(array_filter($seller['data']['shipment'] ?? [], function ($x) { return !empty($x); }));
            $closestDayIndex = reset($shipmentDays);
        }

        if ($closestDayIndex) {
            // shipmentDate nastavim na datum, kdy bude dalsi zavoz (napr. next Monday, coz mi upravi datum tak, aby tam bylo pristi pondeli)
            $shipmentDate = $shipmentDate->modify('next '.$this->getDaysOfWeek()[$closestDayIndex]);
        }

        return $shipmentDate;
    }

    /**
     * Zvaliduje skladovost produktu vuci DataGo.
     */
    public function checkPurchaseStateStockDataGo(PurchaseState $purchaseState): void
    {
        $items = [];
        // pripravim si pole polozek
        foreach ($purchaseState->getProducts() as $item) {
            if (empty($item->getProduct()->code)) {
                continue;
            }

            $items[] = [
                'number' => $item->getProduct()->code,
                'quantity' => $item->getPieces(),
            ];
        }

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

        $data = [
            'currency' => Contexts::get(CurrencyContext::class)->getActiveId(),
            'items' => [
                'item' => $items,
            ],
        ];

        [$user, $pass] = $this->pompoUtil->getDataGoDefaultCredentials();

        $this->dataGoApi->setCredentials($user, $pass);

        // zkontroluju, ze je DataGo API dostupne, a pokud neni, tak neprovadim kontrolu
        if (!$this->dataGoApi->isApiAvailable()) {
            return;
        }

        try {
            // odesilam request na DataGo
            $result = $this->dataGoApi->checkItems($data);
        } catch (\Throwable $e) {
            if (isDevelopment()) {
                throw $e;
            }

            return;
        }

        if (ArrayUtil::isDictionary($result['items']['item'] ?? [])) {
            $result['items']['item'] = [$result['items']['item']];
        }

        // process result
        $stockInfo = [];
        foreach ($result['items']['item'] ?? [] as $item) {
            $stockInfo[$item['number']] = $item['quantity']['total'] ?? 0;
        }

        // overuji dostupnost podle vysledku z DataGo
        foreach ($purchaseState->getProducts() as $item) {
            if (($stockInfo[$item->getProduct()->code] ?? false) === false) {
                continue;
            }

            $availableQuantity = $stockInfo[$item->getProduct()->code];

            // neni na centranil skladu a ani u dodavatele
            if ($availableQuantity < $item->getPieces()) {
                // muze to ale byt produkt z marketplacu, takze potrebuju jeste zkontrolovat, zda neni skladem
                // u dodavatele kterej je pres marketplace
                if ($this->productUtil->isInStoreMarketplace($item->getProduct(), (float) $item->getPieces())) {
                    continue;
                }

                throw new PompoCartException(
                    // Produkt "%s" není v požadovaném množství skladem.
                    sprintf(translate('productNotInStore', 'pompo'), $item->getName())
                );
            }
        }
    }

    /**
     * Zvaliduje skladovost vuci DRSu a DataGo.
     *
     * Vyplnene $sellerId znamena, ze objednavam na prodejnu. V opacnem pripade objednavam pres prepravce.
     *
     * 1. Pokud prijde $sellerId, tak nejdriv koukam na sklad dane prodejny a zbytky, ktere nejsou skladem potom overim vuci centrale (DataGo)
     * 2. Pokud neprijde $sellerId, tak validuju pouze vuci centrale (DataGo), protoze z prodejny se neda posilat dopravcem.
     */
    public function checkPurchaseStateStock(PurchaseState $purchaseState, ?int $sellerId = null): void
    {
        if ($sellerId) {
            $branchId = sqlQueryBuilder()
                ->select('JSON_VALUE(s.data, "$.branchId")')
                ->from('stores', 's')
                ->join('s', 'sellers', 'sell', 's.id = sell.id_store')
                ->where(Operator::equals(['sell.id' => $sellerId]))
                ->execute()->fetchOne();

            if (!$branchId) {
                return;
            }

            // kodu produktu v kosiku
            $productCodes = array_filter(array_map(function ($x) { return $x->getProduct() ? ($x->getProduct()->code ?? null) : null; }, $purchaseState->getProducts()));

            $drsStockStatus = [];
            // nactu si skladovost na prodejne
            foreach ($this->drsApi->getStock($productCodes, (string) $branchId) as $item) {
                $drsStockStatus[mb_strtolower($item['VART'])] = (int) ($item['Quantity'] ?? 0);
            }

            // kontroluju skladovost na prodejne
            $dataGoProducts = [];
            foreach ($purchaseState->getProducts() as $item) {
                if (!$item->getProduct()) {
                    continue;
                }

                if (!($code = $item->getProduct()->code ?? null)) {
                    continue;
                }

                $codeLower = mb_strtolower($code);

                // produkt neni skladem na prodejne
                if (($drsStockStatus[$codeLower] ?? false) === false) {
                    // produkt budu jeste validovat vuci centrale
                    $dataGoProducts[] = [
                        'code' => $code,
                        'purchaseItem' => $item,
                        'pieces' => $item->getPieces(),
                    ];
                    continue;
                }

                // produkt je skladem na prodejne, ale neni v dostatecnem mnozstvi
                if ($drsStockStatus[$codeLower] < $item->getPieces()) {
                    // zbytek produktu zvaliduju jeste vuci centrale
                    $dataGoProducts[] = [
                        'code' => $code,
                        'purchaseItem' => $item,
                        'pieces' => ($item->getPieces() - $drsStockStatus[$codeLower]),
                    ];
                }
            }

            // zbytek zvaliduju vuci DataGo, protoze kdyz neco nemam na prodejne, tak to muzu mit na centrale a na
            // prodejnu se to muze prevezt
            if (!empty($dataGoProducts)) {
                $purchaseItems = [];
                foreach ($dataGoProducts as $item) {
                    $originalPurchaseItem = $item['purchaseItem'];
                    $purchaseItem = new ProductPurchaseItem(
                        $originalPurchaseItem->getIdProduct(),
                        $originalPurchaseItem->getIdVariation(),
                        $item['pieces'],
                        $originalPurchaseItem->getPrice(),
                        $originalPurchaseItem->getNote(),
                        $originalPurchaseItem->getIdDiscount()
                    );

                    if ($originalPurchaseItem->getProduct()) {
                        $purchaseItem->setProduct($originalPurchaseItem->getProduct());
                    }

                    $purchaseItems[] = $purchaseItem;
                }

                // spustim validaci zbylych produktu vuci centrale
                $this->checkPurchaseStateStockDataGo(
                    new PurchaseState($purchaseItems)
                );
            }

            return;
        }

        // zvaliduju cely PurchaseState vuci centrale
        $this->checkPurchaseStateStockDataGo($purchaseState);
    }

    /**
     * Vrací ID PurchaseItemu složené z ID produktu a ID varianty.
     */
    private function getPurchaseItemKey(ProductPurchaseItem $item): string
    {
        $key = $item->getIdProduct();
        if ($item->getIdVariation()) {
            $key .= '/'.$item->getIdVariation();
        }

        return (string) $key;
    }

    /**
     * Pomocná funkce, která vrací dny v týdnu.
     */
    private function getDaysOfWeek(): array
    {
        return [
            1 => 'Monday',
            2 => 'Tuesday',
            3 => 'Wednesday',
            4 => 'Thursday',
            5 => 'Friday',
            6 => 'Saturday',
            7 => 'Sunday',
        ];
    }

    /**
     * Pomocná funkce, která slouží k aktivaci konkrétní prodejny.
     */
    private function activateSeller(?int $sellerId, callable $callback)
    {
        if (!$sellerId) {
            return $callback();
        }

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

        $originalSellerId = $sellerContext->getActiveId();
        $sellerContext->activate((string) $sellerId);

        $result = $callback();

        $sellerContext->activate($originalSellerId);

        return $result;
    }
}
