<?php

declare(strict_types=1);

namespace External\PompoBundle\DataGo\Synchronizer;

use Doctrine\DBAL\Connection;
use External\PompoBundle\DataGo\Exception\DataGoException;
use External\PompoBundle\DataGo\Util\DataGoApi;
use External\PompoBundle\Email\OrderInProgressEmail;
use External\PompoBundle\Email\OrderSentEmail;
use External\PompoBundle\Email\OrderShopTransportEmail;
use External\PompoBundle\Util\Order\OrderSynchronizerTrait;
use External\PompoBundle\Util\Order\PompoOrderUtil;
use External\PompoBundle\Util\Ordering\OrderingUtil;
use External\PompoBundle\Util\Ordering\OrderType;
use External\PompoBundle\Util\PompoUtil;
use External\PompoBundle\Util\ProductUtil;
use KupShop\AdminBundle\Util\ActivityLog;
use KupShop\KupShopBundle\Context\ContextManager;
use KupShop\KupShopBundle\Context\CountryContext;
use KupShop\KupShopBundle\Context\CurrencyContext;
use KupShop\KupShopBundle\Context\DomainContext;
use KupShop\KupShopBundle\Context\LanguageContext;
use KupShop\KupShopBundle\Context\VatContext;
use KupShop\KupShopBundle\Util\ArrayUtil;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\KupShopBundle\Util\Logging\SentryLogger;
use KupShop\KupShopBundle\Util\Price\PriceCalculator;
use KupShop\KupShopBundle\Util\Price\ProductPrice;
use KupShop\KupShopBundle\Util\StringUtil;
use KupShop\OrderingBundle\Entity\Order\OrderItem;
use KupShop\OrderingBundle\OrderList\OrderList;
use KupShop\OrderingBundle\Util\Order\OrderInfo;
use KupShop\OrderingBundle\Util\Order\OrderItemInfo;
use KupShop\PricelistBundle\Context\PricelistContext;
use KupShop\ReturnsBundle\Entity\ReturnEntity;
use KupShop\ReturnsBundle\Util\ReturnsUtil;
use KupShop\SynchronizationBundle\Rabbit\Rabbit;
use KupShop\SynchronizationBundle\Rabbit\RabbitConsumerConfiguration;
use KupShop\SynchronizationBundle\Util\Rabbit\RabbitConsumer;
use Query\Operator;
use Query\Order;
use Query\QueryBuilder;
use Symfony\Contracts\Service\Attribute\Required;

class OrderSynchronizer extends AbstractSynchronizer
{
    use OrderSynchronizerTrait {
        getOrderNote as private getSharedOrderNote;
    }

    protected static $type = 'order';

    /** @required */
    public OrderingUtil $orderingUtil;
    /** @required */
    public OrderList $orderList;
    /** @required */
    public ContextManager $contextManager;
    /** @required */
    public PompoUtil $pompoUtil;
    /** @required */
    public ProductUtil $productUtil;
    /** @required */
    public OrderItemInfo $orderItemInfo;
    /** @required */
    public SentryLogger $sentryLogger;

    #[Required]
    public PompoOrderUtil $pompoOrderUtil;

    #[Required]
    public RabbitConsumer $rabbitConsumer;

    private ?ReturnsUtil $returnsUtil = null;

    /** @required */
    public function setReturnsUtil(?ReturnsUtil $returnsUtil = null): void
    {
        $this->returnsUtil = $returnsUtil;
    }

    public function process(): void
    {
        $this->processOrdersToDataGo();
        $this->processFromRabbit();
    }

    protected function processItem(array $item): void
    {
        $this->logger->log('[DataGo] Processing order change from rabbit', [
            'item' => $item,
        ]);

        // invalid data
        if (empty($item['uid'])) {
            return;
        }

        $orderId = sqlQueryBuilder()
            ->select('do.id_order')
            ->from('datago_orders', 'do')
            ->join('do', 'orders', 'o', 'o.id = do.id_order')
            ->where(Operator::equals(['do.id_datago' => $item['uid']]))
            ->andWhere(Operator::not(Operator::equals(['o.status_storno' => 1])))
            ->andWhere('JSON_VALUE(COALESCE(o.note_admin, "{}"), "$.sendToDRS") IS NULL')
            ->andWhere(Operator::not(
                Operator::findInSet(['NP', 'O'], 'o.flags', 'OR')))
            ->execute()->fetchOne();

        // order not found
        if (!$orderId) {
            return;
        }

        $order = \Order::get($orderId);

        $this->processOrderItem(
            $order,
            $this->prepareDataGoOrder($item)
        );
    }

    public function processFromRabbit(): void
    {
        $this->rabbitConsumer->consume(
            RabbitConsumerConfiguration::create($this->getRabbitQueue()),
            function (\AMQPEnvelope $envelope) {
                $this->processItem(
                    json_decode($envelope->getBody() ?: '', true) ?: []
                );

                return true;
            }
        );
    }

    /**
     * Kontrola stavů objednávek vůči DataGo.
     *
     * $handledCheck
     *  - pokud true, tak se kontrolují pouze objednávky ve stavu "vyřízeno", které je potřeba kontrolovat ještě
     *    kvůli přepnutí do stavu "ukočená".
     *  - pokud false, tak se kontrolují pouze nevyřízené objednávky
     *
     * @deprecated should be replaced by `processFromRabbit`
     */
    public function processOrdersFromDataGo(bool $handledCheck = false): void
    {
        // TODO: odebrat pouzitit tehle fce

        // - Expedováno (byl předán příkaz skladu k přípravě objednávky) – v eshopu nyní jako stav Zpracovává se – zákazníkovi odchází email, že jeho objednávka je právě zpracovávána ve skladu
        // - Fakturováno – v eshopu nyní jako stav Převoz - v tuto chvíli odchází zákazníkovi email o tom, že bude balíček v nejbližším termínu zavezen na prodejnu, což není úplně v pořádku, jelikož mezi dobou, kdy to zabalí a kdy bude zboží skutečně odesláno může být prodleva až několik dní.

        $startTime = microtime(true);

        foreach ($this->getOrders($handledCheck, true, 40) as $batch) {
            // ofiltruju jen objednavky, ktere mam ted kontrolovat
            $ordersToCheck = array_filter($batch->toArray(), function (\Order $x) {
                $data = json_decode($order->datago_data ?? '', true) ?? [];
                if ($data['nextCheckTimestamp'] ?? false) {
                    // pokud ma nastaveny nextCheckTimestamp a aktualni timestamp je mensi, jak ten ulozeny, tak preskakuju kontrolu
                    // a budu kontrolovat az po tom, co bude aktualni timestamp vetsi
                    if (time() < $data['nextCheckTimestamp']) {
                        return false;
                    }
                }

                return true;
            });

            // nemam co prochazet
            if (empty($ordersToCheck)) {
                continue;
            }

            $dataGoIds = array_map(fn (\Order $x) => $x->id_datago ?? null, $ordersToCheck);
            try {
                $dataGoData = $this->dataGoApi->checkOrderBatch(array_filter($dataGoIds));
            } catch (DataGoException $e) {
                $this->logger->logException($e, 'Hromadné načtení objednávek z DataGo selhalo!');
            }

            /** @var \Order $order */
            foreach ($ordersToCheck as $key => $order) {
                // nastavim $result na null, aby se mi do ActivityLogu nezapsal $result z predesle objednavky
                $result = null;

                // pokud nemam DataGo id, tak skipuju
                if (!($dataGoId = ($order->id_datago ?? null))) {
                    continue;
                }

                $data = json_decode($order->datago_data ?? '', true) ?? [];
                $checkTryCount = $data['retryCount'] ?? 1;

                if ($data['nextCheckTimestamp'] ?? false) {
                    // pokud ma nastaveny nextCheckTimestamp a aktualni timestamp je mensi, jak ten ulozeny, tak preskakuju kontrolu
                    // a budu kontrolovat az po tom, co bude aktualni timestamp vetsi
                    if (time() < $data['nextCheckTimestamp']) {
                        continue;
                    }
                }

                if (!empty($dataGoData[$dataGoId])) {
                    $result = $dataGoData[$dataGoId];
                } else {
                    if (!($result = $this->getDataGoOrder($order, $dataGoId))) {
                        continue;
                    }
                }

                if (!$result) {
                    addActivityLog(
                        ActivityLog::SEVERITY_ERROR,
                        ActivityLog::TYPE_SYNC,
                        '[DataGo] Objednávku se nepodařilo sesynchronizovat s DataGo, protože nebyla v DataGo nalezena',
                        [
                            'id_order' => $order->id,
                            'id_datago' => $dataGoId,
                            'result' => $result,
                        ]
                    );
                    continue;
                }

                $status = $this->getDataGoOrderStatus($order, $result);

                // pokud je invoiced a chybi mi barcodes (znamena to, ze jsem objednavku nacetl pres hromadne nacteni, kde ale chybi tenhle udaj a ja ho potrebuju...)
                if ($status['invoiced'] === true && !isset($result['barcodes']) && empty($order->package_id)) {
                    if (!($result = $this->getDataGoOrder($order, $dataGoId))) {
                        continue;
                    }
                }

                $this->processOrderItem($order, $result);

                // ulozim si timestamp posledniho checku
                $this->setOrderNextCheck($order, (int) $checkTryCount);
            }

            $currentTotalTime = (microtime(true) - $startTime);
            // pokud bezim uz vic jak 600s, tak to ukoncim, at se mohou spustit i dalsi veci navazane na cron
            if ($handledCheck === false && $currentTotalTime >= 600) {
                break;
            }
        }
    }

    protected function getRabbitQueue(): string
    {
        if ($this->configuration->isNejhracka()) {
            return 'wpj.Q.nejhracka-datago';
        }

        return 'wpj.Q.pompo-datago';
    }

    private function processOrderItem(\Order $order, array $item): void
    {
        $orderPrevStatus = $order->status;

        // pokud je to vratkova objednavka, tak si nactu vratku
        $return = $this->getOrderReturn($order);
        $status = $this->getDataGoOrderStatus($order, $item);

        // objednavka byla zrusena
        if ($status['cancelled'] === true) {
            if ($order->status_storno != 1) {
                $this->setOrderCompleted($order);
                $this->setMappingData((int) $order->id, 'stornoFromDataGo', 1);
                $order->logHistory('[DataGo] Objednávka byla stornována v DataGo');
                $order->storno(false, null, ($order->getFlags()['DSE'] ?? false) ? false : null);
            }

            return;
        }

        // Dokončené objednávky neaktualizujeme
        if ($this->isCompleted($item)) {
            return;
        }

        // mame cislo baliku
        if (!empty($item['barcodes']) && empty($order->package_id)) {
            if (ArrayUtil::isVector($item['barcodes'])) {
                $barcode = array_map(fn ($x) => $x['barcode'], $item['barcodes']);
            } else {
                $barcode = $item['barcodes']['barcode'] ?? [];
            }

            if (!empty($barcode)) {
                // objednavka muze mit vice cisel baliku
                if (is_array($barcode)) {
                    $barcode = implode(',', $barcode);
                }

                $this->setOrderPackageNumber($order, $barcode);
            }
        }

        // DataGo Expedovano: predan prikaz skladu k priprave objednavky - zpracovava se
        if ($status['dispatched'] === true) {
            $emailDispatched = OrderInProgressEmail::getType();
            // pokud se jedna o vratku, tak neposilam mail
            if ($return) {
                $emailDispatched = null;
            }

            $this->changeOrderStatus($order, 2, $emailDispatched);
        }

        // DataGo Fakturovano: pro osobni odber to znamena prevoz, pro dopravce to znamena, ze byla objednavka odeslana
        if ($status['invoiced'] === true) {
            if ($this->isOrderInPerson($order)) {
                // prepnout do stavu "prevoz" + odeslat notifikaci
                $this->changeOrderStatus($order, 3, OrderShopTransportEmail::getType());
                // objednavka by se mela odeslat do DRSu, protoze tam jeste neni, takze si na ni nastavim custom data, kteryma to reknu
                // DRS synchronizace na to potom sahne a odesle to
                $order->setData('sendToDRS', 1);
            } else {
                $emailInvoiced = OrderSentEmail::getType();
                // pokud se jedna o vratku, tak neposilam email o vyrizeni objednavky
                if ($return) {
                    $emailInvoiced = null;
                }

                // prepnout do vyrizeno a odeslat email s cislem baliku
                $this->changeOrderStatus($order, 4, $emailInvoiced);
            }

            // nastavim vratce stav na vyrizeno, aby se odeslal mail o vyrizeni vratky
            if ($return) {
                if ($return->getStatus() != 2) {
                    $this->returnsUtil->changeStatus($return->getId(), 2);
                    $order->logHistory('[DataGo] Vratková objednávka byla vyřízena a stav vratky byl změněn na vyřízeno.');
                }
            }

            // pokud byla v DataGo oznacena jako zaplacena, tak je objednavka hotova a ukoncena
            if ($status['paid'] === true) {
                $order->logHistory('[DataGo] Objednávka kompletní - Hotová a ukončená');
                $this->changeOrderStatus($order, $this->configuration->getOrderFinalStatus());
                $this->setOrderCompleted($order);
            }

            // aktualizace polozek objednavky podle DataGo
            if (!in_array($orderPrevStatus, getStatuses('handled'))) {
                // aktualizace polozek objednavky podle DataGo - provadim ji az kdyz je objednavka ve stavu "invoiced"
                // protoze az v tu chvili vim, ktere polozky byyl vyfakturovany a ktere nebyly
                $this->updateOrderItemsFromDataGo($order, $item);
            }
        }
    }

    private function getDataGoOrder(\Order $order, string $dataGoId): ?array
    {
        [$user, $pass] = $this->pompoUtil->getDataGoCredentialsByOrder($order);

        try {
            $result = $this->dataGoApi->setCredentials($user, $pass)
                ->checkOrder($dataGoId);
        } catch (\Throwable $e) {
            $this->logger->logException($e, sprintf('Chyba při zpětném stahování objednávky %s', $order->order_no));

            $result = null;
        }

        return $result;
    }

    /**
     * @deprecated Melo by se prestat pouzivat, protoze se prestane pouzivat `processOrdersFromDataGo`
     */
    private function getOrders(bool $handledCheck = false, bool $returnAsBatch = false, int $batchSize = 200): iterable
    {
        $orderList = clone $this->orderList;

        $orderList
            ->fetchItems()
            ->andSpec(function (QueryBuilder $qb) use ($handledCheck) {
                $qb->addSelect('do.id_datago, do.data as datago_data')
                    ->join('o', 'datago_orders', 'do', 'do.id_order = o.id');

                $specs = [
                    // pouze nestornovane objednavky
                    Operator::equals(['o.status_storno' => 0]),
                    // pouze objednavky, ktere jeste nejsou oznacene jako vyrizene
                    'JSON_VALUE(COALESCE(do.data, "{}"), "$.completed") IS NULL',
                    // Objednavka typu rezervace s prepravou se ma odeslat do DRSu a od te doby nas nezajima jeji stav v DataGo
                    'JSON_VALUE(COALESCE(o.note_admin, "{}"), "$.sendToDRS") IS NULL',
                    // Nakupy na prodejne a stare objednavky nas nezajimaji
                    Operator::not(
                        Operator::findInSet(['NP', 'O'], 'o.flags', 'OR')
                    ),
                ];

                if ($this->configuration->isNejhracka()) {
                    // beru v potaz pouze objednavky vytvorene pred 30.11., protoze novejsi objednavky jedou uz pres Rabbit frontu
                    $specs[] = 'o.date_created <= "2023-12-05 23:59:59"';
                }

                if ($this->configuration->isPompo()) {
                    // beru v potaz pouze objednavky vytvorene pred 1.2., protoze novejsi objednavky jedou uz pres Rabbit frontu
                    $specs[] = 'o.date_created <= "2024-02-01 23:59:59"';
                }

                if ($handledCheck) {
                    // beru pouze objednavky ve stavu 4 - vyřízeno, a kontroluji je kvuli prepnuti do stavu ukončená
                    $specs[] = Operator::equals(['o.status' => 4]);
                } else {
                    // beru pouze nevyrizene objednavky
                    $specs[] = Operator::not(Operator::inIntArray(getStatuses('handled'), 'o.status'));
                }

                return Operator::andX($specs);
            });

        // potrebuju nacist totalcount
        $orderList->limit(1);
        $orderList->getOrders($totalCount);

        // nacitam objednavky
        $iterationLimit = $totalCount / $batchSize;
        for ($i = 0; $i < $iterationLimit; $i++) {
            $orderList
                ->limit($batchSize, $i * $batchSize);

            $orders = $orderList->getOrders();

            if ($returnAsBatch) {
                yield $orders;
            } else {
                foreach ($orders as $order) {
                    yield $order;
                }
            }
        }
    }

    private function processOrdersToDataGo(): void
    {
        if (isDevelopment()) {
            return;
        }

        $qb = sqlQueryBuilder()
            ->select('o.id')
            ->from('orders', 'o')
            ->leftJoin('o', 'datago_orders', 'do', 'do.id_order = o.id')
            // objednavky, ktere se jeste neodeslaly
            ->andWhere('do.id_datago IS NULL')
            // nestornovane objednavky
            ->andWhere(Operator::equals(['o.status_storno' => 0]))
            // objednavka musi byt zaplacena, nebo nemit platbu online nebo prevodem
            ->andWhere(
                Operator::orX(
                    // bud je to platba online / prevodem a je zaplacena
                    Operator::andX(
                        Order::byPaymentMethods([\Payment::METHOD_ONLINE, \Payment::METHOD_TRANSFER]),
                        Operator::equals(['o.status_payed' => 1])
                    ),
                    // nebo to neni platba online / prevodem
                    Operator::not(
                        Order::byPaymentMethods([\Payment::METHOD_ONLINE, \Payment::METHOD_TRANSFER])
                    ),
                    // nebo to je expando objednavka
                    Operator::findInSet(['DSE'], 'o.flags'),
                    // nebo to je vratkova objednavka
                    Operator::equals(['o.source' => OrderInfo::ORDER_SOURCE_RETURNS])
                )
            )
            // objednavka nema flag NP - Nakup na prodejne, nebo O - Historicka objednavka
            ->andWhere(
                Operator::not(
                    Operator::findInSet(['NP', 'O'], 'o.flags', 'OR')
                )
            )
            // je to bud objednavka pres prepravce nebo castecne pres prepravce
            // nebo nema type (nejhracka)
            // cista rezervace se do DataGo neodesila - ta jde jen do DRSu
            ->andWhere(
                Operator::orX(
                    $this->getOrderTypeSpec([OrderType::ORDER_TRANSPORT, OrderType::ORDER_TRANSPORT_RESERVATION]),
                    $this->getOrderTypeSpec(null),
                )
            );

        foreach ($qb->execute() as $item) {
            $order = $this->withDefaultContexts(fn () => \Order::get((int) $item['id']));

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

            $result = null;
            try {
                $orderData = $this->getOrderData($order);

                $this->logger->log('Data go order synchronizace před', [
                    'orderData' => $orderData,
                ]);

                $result = $this->dataGoApi->setCredentials($user, $pass)
                    ->createOrder(
                        $orderData
                    );

                $this->logger->log('Data go order synchronizace po', [
                    'orderData' => $orderData,
                    'result' => $result,
                ]);

                if (!($result['uniqueid'] ?? false)) {
                    throw new DataGoException('Order was not sent to DataGo!');
                }

                $order->changeStatus(
                    1,
                    sprintf('[DataGo] Objednávka byla úspěšně nahrána do DataGo: %s; zapsáno pod uživatele: %s', $result['uniqueid'], $user),
                    false
                );

                sqlQueryBuilder()
                    ->insert('datago_orders')
                    ->directValues(
                        [
                            'id_datago' => $result['uniqueid'],
                            'id_order' => $order->id,
                        ]
                    )->execute();
            } catch (\Throwable $e) {
                $this->logger->logException(
                    $e,
                    sprintf('[DataGo] Objednávku %s se nepodařilo zapsat', $order->order_no),
                    [
                        'message' => $e->getMessage(),
                        'result' => $result ?? [],
                    ]
                );

                // pokud DataGo vrati chybu, ze objednavka je uz zapsana
                if (str_contains($e->getMessage(), $this->getOrderNumber($order).' již existuje')) {
                    $order->changeStatus(
                        1,
                        sprintf('[DataGo] Objednávka byla úspěšně nahrána do DataGo: %s; zapsáno pod uživatele: %s', $this->getOrderUniqueId($order), $user),
                        false
                    );

                    sqlQueryBuilder()
                        ->insert('datago_orders')
                        ->directValues(
                            [
                                'id_datago' => $this->getOrderUniqueId($order),
                                'id_order' => $order->id,
                            ]
                        )->execute();
                }
            }
        }
    }

    public function getOrderData(\Order $order): array
    {
        $result = [
            'uniqueid' => $this->getOrderUniqueId($order),
            'order_number' => $this->getOrderNumber($order),
            'currency' => $order->getCurrency(),
            'message' => $this->getOrderNote($order),
            'delivery' => $this->getDeliveryCode($order),
            'payment' => $this->getPaymentCode($order),
            'customer' => [
                'email' => $order->invoice_email,
                'phone' => $order->invoice_phone,
                'billingaddress' => $this->getCustomerAddress($order),
                'shippingaddress' => $this->getCustomerAddress($order, 'delivery'),
            ],
            'items' => [
                'item' => [],
            ],
        ];

        $delivery = $order->getDeliveryType()->getDelivery();
        if ($delivery->getType() === \Delivery::TYPE_POINT) {
            $result['delivery_comment'] = $delivery->getPointId();
        }

        if ($vatSubtype = $this->getOrderVatSubtype($order)) {
            $result['vatsubtype'] = $vatSubtype;
        }

        // Pokud je to objednavka se zavozem na prodejnu, tak musim plnit datum zavozu
        if ($this->orderingUtil->getOrderType($order) === OrderType::ORDER_TRANSPORT_RESERVATION) {
            // deliveryDate mam ulozeny v datech delivery_data na objednavce
            $deliveryData = $order->getData('delivery_data');
            if ($deliveryData['deliveryDate'] ?? false) {
                $result['extra']['expected_tod'] = $deliveryData['deliveryDate'];
            }
        }

        $deliveryItem = null;
        /** @var OrderItem $item */
        foreach ($order->fetchItems() as $item) {
            if (!($item instanceof OrderItem)) {
                $this->logger->log('OrderItem neni OrderItem ale '.gettype($item).'!!!', [
                    'item' => $item,
                    'order' => $order,
                    'result' => $result,
                ]);
            }

            $isChargeItem = !empty($item->getNote()['charge']);

            if (!$isChargeItem && $this->orderItemInfo->getItemType($item) === OrderItemInfo::TYPE_DELIVERY) {
                $deliveryItem = $item;
                continue;
            }

            $product = $item->getProduct();

            $code = '99WEB';
            if ($isChargeItem && ($chargeItemCode = $this->pompoOrderUtil->getOrderItemChargeCode($item))) {
                $code = $chargeItemCode;
            }

            if ($item->getProductId()) {
                // pokud mam product id, ale z nejakeho duvodu se nepovedlo nacist produkt, tak hodim chybu
                // na nejhracce vznika problem, kdy se u normalni polozky strci do kodu produktu 99WEB, coz by nemelo
                // kdyz se jedna o produktovou polozku
                if (!$product) {
                    $this->logger->log('[DataGo] Order item error - '.$order->order_no, [
                        'orderId' => $order->id,
                        'item' => $item->getItem(),
                    ]);

                    throw new \RuntimeException('DataGo: Objednavka '.$order->order_no.' - U produktove polozky se nepodarilo nacist produkt! ');
                }

                $code = $product->code;
            }

            // doprava od zakaznika pri vratkach
            if ($isReturnDelivery = $this->orderItemInfo->getItemType($item) === OrderItemInfo::TYPE_RETURN_DELIVERY) {
                $code = 'DOP101';
            }

            $isTransportCharge = $this->isOrderItemTransportCharge($item);
            // manipulacni poplatek
            if ($isTransportCharge) {
                $code = '995003';
            }

            $orderItem = [
                'id' => $item->getId(),
                'number' => $code,
                'price' => $item->getPiecePrice()->getPriceWithVat(false)->value(),
                'vat' => $item->getVat(),
                'quantity' => $item->getPieces(),
            ];

            // pokud se jedna o doprava od zakaznina pri vratce, tak chteji posilat cenu a kusy jako zapornou hodnotu
            // aby to bylo na dobropisu spravne vycislene
            if ($isReturnDelivery) {
                $orderItem['price'] = toDecimal($orderItem['price'])->mul(\DecimalConstants::negativeOne())->value();
                $orderItem['quantity'] = toDecimal($orderItem['quantity'])->mul(\DecimalConstants::negativeOne())->asFloat();
            }

            if ($this->isOrderItemExternal($item)) {
                $orderItem['specification'] = $this->getOrderItemSpecification($order, $item);

                if ($vendorId = $this->getOrderItemVendor($item)) {
                    $orderItem['vendor'] = $vendorId;
                }
            }

            $itemInfo = $order->getItemInfo($item);
            if ($itemInfo['selectedVariation'] ?? false) {
                $orderItem['comment'] = $itemInfo['selectedVariation'];
            }

            if ($vatSubtype = $this->getOrderVatSubtype($order)) {
                $orderItem['vatsubtype'] = $vatSubtype;
            }

            if (!$product && !$isTransportCharge && !$isChargeItem) {
                $orderItem['comment'] = $item->getDescr();
            }

            $result['items']['item'][] = $orderItem;
        }

        // delivery and payment item
        foreach ($this->getDeliveryAndPaymentItem($order, $deliveryItem) as $item) {
            $result['items']['item'][] = $item;
        }

        return $result;
    }

    protected function updateOrderItemsFromDataGo(\Order $order, array $dataGoOrder): void
    {
        $diffResult = $this->pompoOrderUtil->getOrderItemDifferenceResult(
            $this->pompoOrderUtil->getOrderItemsCounts($order),
            DataGoApi::prepareOrderCurrentItemsCounts($dataGoOrder)
        );

        if (!$diffResult['hasDiff']) {
            return;
        }

        $this->pompoOrderUtil->updateOrderByDifference($order, $diffResult);
    }

    protected function getOrderNote(\Order $order): string
    {
        $note = $this->getSharedOrderNote($order);

        $additionalNotes = [];

        // pokud se jedna o vratku
        if ($return = $this->getOrderReturn($order)) {
            if (!empty($return->getBankAccount())) {
                $additionalNotes[] = 'Vrácené zboží, peníze zaslat na účet: '.$return->getBankAccount();
            }

            $orderIds = array_map(fn ($x) => $x['id_order'], $return->getItems());
            if ($originalOrderId = reset($orderIds)) {
                try {
                    $originalOrder = \Order::get((int) $originalOrderId);
                    $additionalNotes[] = 'Vratka se vztahuje k objednávce č.: '.$originalOrder->order_no;
                } catch (\InvalidArgumentException $e) {
                }
            }
        }

        return implode('; ', array_merge($additionalNotes, [$note]));
    }

    /**
     * Vrací `null` pokud se má vzít skladovost z hlavního skladu, nebo vrátí číslo dodavatele
     * od kterého se má skladovost vzít.
     */
    protected function getOrderItemVendor(OrderItem $item): ?string
    {
        if (!$item->getProductId()) {
            return null;
        }

        // pokud je produkt skladem na hlavnim skladu, tak beru odtamtud (nespecifikuji tedy sklad)
        $mainStoreQuantity = (float) sqlQueryBuilder()
            ->select('quantity')
            ->from('stores_items')
            ->where(Operator::equals(['id_store' => StockSynchronizer::STORE_ID, 'id_product' => $item->getProductId()]))
            ->execute()->fetchOne();

        if ($mainStoreQuantity >= $item->getPieces()) {
            return null;
        }

        $data = $item->getProduct()->getData();
        $marketplaceSuppliers = $this->configuration->getMarketplaceSuppliers();

        $qb = sqlQueryBuilder()
            ->select('s.id, s.data')
            ->from('products_of_suppliers', 'pos')
            ->join('pos', 'suppliers', 's', 's.id = pos.id_supplier')
            ->andWhere(Operator::equals(['pos.id_product' => $item->getProductId()]))
            ->andWhere(Operator::inIntArray(array_keys($marketplaceSuppliers), 'pos.id_supplier'))
            ->andWhere('pos.in_store > 0')
            ->groupBy('s.id');

        if (!empty($data['mainSuppliers'])) {
            $qb->addOrderBy('FIELD(pos.id_supplier, :mainSuppliers)')
                ->addParameters(['mainSuppliers' => array_values($data['mainSuppliers'])], ['mainSuppliers' => Connection::PARAM_INT_ARRAY]);
        }

        $qb->addOrderBy('COALESCE(pos.price_buy, 9999999)', 'ASC');

        $suppliers = $qb->execute()->fetchAllAssociative();

        if ($supplier = reset($suppliers)) {
            $supplierData = json_decode($supplier['data'] ?: '', true) ?: [];

            return $supplierData['id_datago'] ?? null;
        }

        return null;
    }

    /**
     * Element obsahující definice položky pro její vytvoření. Pokud je přítomen a položka není
     * v systému dohledána, bude vytvořena automaticky dle zde uvedených hodnot.
     */
    protected function getOrderItemSpecification(\Order $order, OrderItem $item): array
    {
        return $this->withDefaultContexts(function () use ($item) {
            $parameterEan = 93;

            $product = $item->getProduct();
            // nacist EANs parametr
            $product->fetchParameters($parameterEan);

            $supplierData = sqlQueryBuilder()
                ->select('s.data')
                ->from('products_of_suppliers', 'pos')
                ->join('pos', 'suppliers', 's', 's.id = pos.id_supplier')
                ->where(
                    Operator::andX(
                        Operator::equals(['pos.id_product' => $item->getProductId()]),
                        Operator::not(Operator::inIntArray($this->configuration->getDefaultSupplierId(), 'pos.id_supplier')),
                        'pos.in_store > 0'
                    )
                )
                ->execute()->fetchOne();

            $supplierData = json_decode($supplierData ?: '', true) ?: [];

            $priceCzk = $this->getProductPrice(
                productId: $item->getProductId(),
                language: 'cs',
                currencyId: 'CZK'
            );

            $priceEur = $this->getProductPrice(
                productId: $item->getProductId(),
                language: 'sk',
                currencyId: 'EUR'
            );

            $eans = [$product->ean];
            foreach ($product->param[$parameterEan]['values'] ?? [] as $param) {
                $eans[] = $param['value'];
            }

            $dimensions = [];

            if ($product->weight) {
                $dimensions[] = [
                    '@attributes' => [
                        'type' => 100,
                    ],
                    '@value' => toDecimal($product->weight)->printFloatValue(2),
                ];
            }

            if ($product->width) {
                $dimensions[] = [
                    '@attributes' => [
                        'type' => 150,
                    ],
                    '@value' => toDecimal($product->width)->printFloatValue(2),
                ];
            }

            if ($product->height) {
                $dimensions[] = [
                    '@attributes' => [
                        'type' => 160,
                    ],
                    '@value' => toDecimal($product->height)->printFloatValue(2),
                ];
            }

            if ($product->depth) {
                $dimensions[] = [
                    '@attributes' => [
                        'type' => 170,
                    ],
                    '@value' => toDecimal($product->depth)->printFloatValue(2),
                ];
            }

            $customs = [];

            if (!empty($product->id_cn)) {
                $cnCategory = sqlQueryBuilder()
                    ->select('cn')
                    ->from('kupshop_shared.oss_vats_categories')
                    ->where(Operator::equals(['cnkey' => $product->id_cn]))
                    ->execute()->fetchOne();

                if (!empty($cnCategory)) {
                    $customs['number'] = str_replace(' ', '', $cnCategory);
                }
            }

            return [
                'name' => mb_substr($product->title, 0, 45),
                'vat' => $product->vat,
                'barcode' => array_filter(array_unique($eans)),
                'customs' => $customs,
                'dim' => $dimensions,
                'purchase_price' => [
                    '@attributes' => [
                        'vendor' => $supplierData['id_datago'] ?? '',
                        'currency' => Contexts::get(CurrencyContext::class)->getDefaultId(),
                    ],
                    '@value' => $product->price_buy ? $product->price_buy['value_without_vat']->printFloatValue(2) : 0,
                ],
                'sales_price' => [
                    // CZK cena s DPH
                    [
                        '@attributes' => [
                            'type' => 2,
                            'vat' => 1,
                            'currency' => $priceCzk->getCurrency()->getId(),
                        ],
                        '@value' => $priceCzk->getPriceWithVat(false)->printFloatValue(2),
                    ],
                    // CZK cena bez DPH
                    [
                        '@attributes' => [
                            'type' => 2,
                            'vat' => 0,
                            'currency' => $priceCzk->getCurrency()->getId(),
                        ],
                        '@value' => $priceCzk->getPriceWithoutVat(false)->printFloatValue(2),
                    ],
                    // EUR cena s DPH
                    [
                        '@attributes' => [
                            'type' => 5,
                            'vat' => 1,
                            'currency' => $priceEur->getCurrency()->getId(),
                        ],
                        '@value' => $priceEur->getPriceWithVat(false)->printFloatValue(2),
                    ],
                    // EUR cena bez DPH
                    [
                        '@attributes' => [
                            'type' => 5,
                            'vat' => 0,
                            'currency' => $priceEur->getCurrency()->getId(),
                        ],
                        '@value' => $priceEur->getPriceWithoutVat(false)->printFloatValue(2),
                    ],
                ],
                'market_country' => ['CZ', 'SK'],
            ];
        });
    }

    /**
     * Vrací, zda je položka objednávky od externího dodavatele - marketplace.
     */
    private function isOrderItemExternal(OrderItem $item): bool
    {
        if ($product = $item->getProduct()) {
            $suppliers = array_filter(
                $product->getSuppliers()[0] ?? [],
                fn ($x) => !in_array($x, $this->configuration->getDefaultSupplierId()),
                ARRAY_FILTER_USE_KEY
            );

            if (!empty($suppliers)) {
                return true;
            }
        }

        return false;
    }

    private function getDataGoOrderStatus(\Order $order, array $dataGoOrder): array
    {
        if (ArrayUtil::isDictionary($dataGoOrder['items']['item'] ?? [])) {
            $dataGoOrder['items']['item'] = [$dataGoOrder['items']['item']];
        }

        $result = [
            'cancelled' => false,
            'dispatched' => false,
            'delivered' => false,
            'invoiced' => false,
            'paid' => false,
        ];

        foreach ($dataGoOrder['items']['item'] ?? [] as $item) {
            if (StringUtil::startsWith($item['id'], 'P') || StringUtil::startsWith($item['id'], 'D')) {
                continue;
            }

            // zajimaji me jenom produkty
            if (!($orderItem = ($order->fetchItems()[$item['id'] ?? null]['id_product'] ?? null))) {
                continue;
            }

            if ($item['dispatched'] === 'true' || $item['dispatched'] === true) {
                $result['dispatched'] = true;
            }

            if ($item['delivered'] === 'true' || $item['delivered'] === true) {
                $result['delivered'] = true;
            }

            if ($item['invoiced'] === 'true' || $item['invoiced'] === true) {
                $result['invoiced'] = true;
            }

            if ($item['paid'] === 'true' || $item['paid'] === true) {
                $result['paid'] = true;
            }
        }

        if ($dataGoOrder['cancelled'] === 'true' || $dataGoOrder['cancelled'] === true) {
            $result['cancelled'] = true;
        }

        if ($dataGoOrder['dispatched'] === 'true' || $dataGoOrder['dispatched'] === true) {
            $result['dispatched'] = true;
        }

        if ($dataGoOrder['delivered'] === 'true' || $dataGoOrder['delivered'] === true) {
            $result['delivered'] = true;
        }

        if ($dataGoOrder['invoiced'] === 'true' || $dataGoOrder['invoiced'] === true) {
            $result['invoiced'] = true;
        }

        if ($dataGoOrder['paid'] === 'true' || $dataGoOrder['paid'] === true) {
            $result['paid'] = true;
        }

        return $result;
    }

    protected function getOrderUniqueId(\Order $order): string
    {
        $prefix = 'POM';
        if ($this->configuration->isNejhracka()) {
            $prefix = 'NEJ';
        }

        return (isDevelopment() ? 'TEST_' : '').$prefix.'_WPJ'.$order->id;
    }

    public function setOrderCompleted(\Order $order): void
    {
        // mark as completed
        $this->setMappingData((int) $order->id, 'completed', 1);
    }

    /**
     * Ulozi si k objednavce timestamp, kdy naposledy provedl check stavu vuci DataGo. Kvuli rychlosti to
     * nechceme spoustet kazdych 5 minut, ale treba jen jednou za 20 minut, aby to zbytecne nezdrzovalo.
     */
    public function setOrderLastCheck(\Order $order): void
    {
        $this->setMappingData((int) $order->id, 'lastCheckTimestamp', time());
    }

    public function setOrderNextCheck(\Order $order, int $retryCount = 1): void
    {
        // max pocet pokusu 6, aby ten cas nesel strasne vysoko
        $retryCount = min($retryCount, 6);
        // timestamp + 25 minut + random minuty podle poctu pokusu
        $nextCheckTimestamp = (int) (time() + 1500 + (pow(2, $retryCount) * 60) * (rand(50, 150) / 100));

        $this->setMappingData((int) $order->id, 'nextCheckTimestamp', $nextCheckTimestamp);
        $this->setMappingData((int) $order->id, 'retryCount', $retryCount);
    }

    public function setMappingData(int $orderId, string $key, $value): void
    {
        sqlQueryBuilder()
            ->update('datago_orders')
            ->set('data', 'JSON_SET(COALESCE(data, "{}"), "$.'.$key.'", :value)')
            ->setParameter('value', $value)
            ->where(
                Operator::equals(
                    [
                        'id_order' => $orderId,
                    ]
                )
            )->execute();
    }

    private function getDeliveryAndPaymentItem(\Order $order, ?OrderItem $deliveryItem): array
    {
        $result = $this->splitDeliveryItem($order, $deliveryItem);

        if ($result['delivery'] ?? false) {
            // manipulacni poplatek nebo dopravne
            $result['delivery']['number'] = $this->isOrderInPerson($order) ? '995003' : 'DOP101';
            if ($vatSubtype = $this->getOrderVatSubtype($order)) {
                $result['delivery']['vatsubtype'] = $vatSubtype;
            }
        }

        if ($result['payment'] ?? false) {
            $result['payment']['number'] = 'DOP102';
            if ($vatSubtype = $this->getOrderVatSubtype($order)) {
                $result['payment']['vatsubtype'] = $vatSubtype;
            }
        }

        return $result;
    }

    private function getPaymentCode(\Order $order): string
    {
        $payment = null;
        if ($deliveryType = $order->getDeliveryType()) {
            $payment = $deliveryType->getPayment();
        }

        $data = $payment->getCustomData();
        if ($data['datago_code'] ?? null) {
            return $data['datago_code'];
        }

        throw new DataGoException(
            sprintf('U platby "%s" není vyplněný kód pro DataGo! Nemohu zapsat objednávku %s do DataGo.', $payment->getName(), $order->order_no)
        );
    }

    private function getDeliveryCode(\Order $order): string
    {
        $delivery = null;
        if ($deliveryType = $order->getDeliveryType()) {
            $delivery = $deliveryType->getDelivery();
        }

        $data = $delivery->getCustomData();
        if ($data['datago_code'] ?? null) {
            return $data['datago_code'];
        }

        throw new DataGoException(
            sprintf('U dopravy "%s" není vyplněný kód pro DataGo! Nemohu zapsat objednávku %s do DataGo.', $delivery->name, $order->order_no)
        );
    }

    private function getCustomerAddress(\Order $order, string $type = 'invoice'): array
    {
        $name = mb_substr(implode(' ', array_filter([$order->{$type.'_name'}, $order->{$type.'_surname'}])), 0, 45);
        $firm = $order->{$type.'_firm'};

        if ($type === 'delivery') {
            if ($deliveryData = $this->getPointDeliveryData($order)) {
                [$deliveryName, $firm] = $deliveryData;
                $name = $deliveryName !== null ? $deliveryName : $name;
            }
        }

        return [
            'name' => $name,
            'company' => $firm,
            'street' => mb_substr($order->{$type.'_street'}, 0, 45),
            'city' => mb_substr($order->{$type.'_city'}, 0, 45),
            'postcode' => $order->{$type.'_zip'},
            'country_code' => $order->{$type.'_country'},
            'companyid' => $type === 'delivery' ? '' : $order->invoice_ico,
            'vatid' => $type === 'delivery' ? '' : $order->invoice_dic,
        ];
    }

    private function getPointDeliveryData(\Order $order): ?array
    {
        $delivery = $order->getDeliveryType()->getDelivery();

        if ($delivery->getType() !== \Delivery::TYPE_POINT) {
            return null;
        }

        if ($delivery instanceof \Zasilkovna || $delivery instanceof \DPDPickup) {
            return null;
        }

        $info = $delivery->getInfo();

        $name = null;
        if ($delivery instanceof \PPLParcelShop) {
            $name = $delivery::$className.' '.($info['code'] ?? $info['id'] ?? '');
        }

        $firm = $info['name'] ?? '';

        return [$name, $firm];
    }

    private function getOrderVatSubtype(\Order $order): ?int
    {
        // pokud je to dropshipment objednavka a je B2B, tak vracim podtyp dph 10
        if (!empty($order->getFlags()['DSE']) && !empty($order->getFlags()['B2B'])) {
            return 10;
        }

        if ($this->isVatSubtypeAllowed($order)) {
            return 120;
        }

        return null;
    }

    /**
     * Vraci true pokud se jedna o zahranici v EU - na zaklade toho nastavujeme jiny typ DPH pro DataGo.
     */
    private function isVatSubtypeAllowed(\Order $order): bool
    {
        if ($this->configuration->isPompo()) {
            return !$this->isOrderInPerson($order) && $order->getLanguage() === 'sk';
        }

        // pokud se jedna o CZ zemi, tak vratim vzdy false
        if ($order->delivery_country === 'CZ') {
            return false;
        }

        return $order->getLanguage() === 'sk' || !empty($order->getFlags()['DSE']);
    }

    private function getOrderReturn(\Order $order): ?ReturnEntity
    {
        if (!$this->returnsUtil) {
            return null;
        }

        if ($order->source !== OrderInfo::ORDER_SOURCE_RETURNS) {
            return null;
        }

        if (!($returnId = $order->getData('id_return'))) {
            return null;
        }

        return $this->returnsUtil->getReturn((int) $returnId) ?: null;
    }

    /** Kvuli zpetne kompatibilite API vs Message pres rabbita */
    private function prepareDataGoOrder(array $dataGoOrder): array
    {
        if (empty($dataGoOrder['items']['item']) && !empty($dataGoOrder['items'])) {
            $items = $dataGoOrder['items'];

            foreach ($items as &$item) {
                if (empty($item['id']) && !empty($item['uid'])) {
                    $item['id'] = $item['uid'];
                }
            }

            $dataGoOrder['items'] = [
                'item' => $items,
            ];
        }

        return $dataGoOrder;
    }

    private function getProductPrice(int $productId, string $language, string $currencyId): ProductPrice
    {
        return $this->contextManager->activateContexts(
            [
                LanguageContext::class => $language,
                DomainContext::class => $this->contextManager->getDomainFromLanguage($language),
                CountryContext::class => ContextManager::CLEAR_CACHE,
                PricelistContext::class => ContextManager::CLEAR_CACHE,
                VatContext::class => ContextManager::CLEAR_CACHE,
            ],
            function () use ($productId, $currencyId) {
                $product = new \Product();
                $product->createFromDB($productId);

                return PriceCalculator::convert(
                    $product->getProductPrice(),
                    Contexts::get(CurrencyContext::class)->getOrDefault($currencyId)
                );
            }
        );
    }

    private function withDefaultContexts(callable $fn): mixed
    {
        $languageContext = Contexts::get(LanguageContext::class);
        $currencyContext = Contexts::get(CurrencyContext::class);

        return $this->contextManager->activateContexts(
            [
                LanguageContext::class => $languageContext->getDefaultId(),
                CurrencyContext::class => $currencyContext->getDefaultId(),
            ],
            $fn
        );
    }

    /**
     * Check if order has status completed in datago_orders.data.
     *
     * @throws \Doctrine\DBAL\Driver\Exception
     * @throws \Doctrine\DBAL\Exception
     */
    private function isCompleted(array $item): bool
    {
        $orderId = sqlQueryBuilder()
            ->select('do.id_order')
            ->from('datago_orders', 'do')
            ->where(Operator::equals(['do.id_datago' => $item['uid']]))
            ->andWhere('JSON_VALUE(COALESCE(do.data, "{}"), "$.completed") IS NULL')
            ->execute()
            ->fetchOne();

        return $orderId ? false : true;
    }
}
