<?php

declare(strict_types=1);

namespace KupShop\DropshipBundle\Transfer;

use KupShop\DropshipBundle\Exception\TransferException;
use KupShop\DropshipBundle\TransferInterface;
use KupShop\DropshipBundle\Util\TransferWorker;
use KupShop\KupShopBundle\Context\CurrencyContext;
use KupShop\KupShopBundle\Context\LanguageContext;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\OrderingBundle\Util\Order\OrderImporter;
use KupShop\OrderingBundle\Util\Order\OrderItemInfo;
use Query\Operator;
use Symfony\Component\ErrorHandler\ErrorHandler;

class GenericTransfer extends AbstractTransfer implements TransferInterface
{
    protected static string $type = 'generic';
    protected static string $name = 'Obecný';

    /** @required */
    public OrderImporter $orderImporter;
    /** @required */
    public TransferWorker $transferWorker;

    public function prepareConfigurationData(array $data): array
    {
        $groups = [];

        foreach ($data['mappingGroups'] ?? [] as $group) {
            if (!empty($group['delete'])) {
                continue;
            }

            $deliveries = [];
            foreach ($group['deliveries'] ?? [] as $delivery) {
                if (!empty($delivery['delete']) || empty($delivery['id_delivery'])) {
                    continue;
                }

                $deliveries[] = $delivery;
            }

            $payments = [];
            foreach ($group['payments'] ?? [] as $payment) {
                if (!empty($payment['delete']) || empty($payment['id_payment'])) {
                    continue;
                }

                $payments[] = $payment;
            }

            // empty values to the end
            uasort($deliveries, fn ($x) => empty($x['value']) ? 1 : -1);
            // empty values to the end
            uasort($payments, fn ($x) => empty($x['value']) ? 1 : -1);

            $group['deliveries'] = $deliveries;
            $group['payments'] = $payments;

            $groups[] = $group;
        }

        $data['mappingGroups'] = $groups;

        return $data;
    }

    protected function getExternalData(\SimpleXMLElement $xml): array
    {
        return [
            (string) $xml->EXTERNAL->ID,
            (array) $xml->EXTERNAL,
        ];
    }

    protected function getDeliveryTypeByConfiguration(\SimpleXMLElement $order): ?\DeliveryType
    {
        $delivery = (string) $order->DELIVERY;
        $payment = (string) $order->PAYMENT;
        $country = (string) $order->DELIVERY_ADDRESS->COUNTRY;

        $mappingGroup = $this->getMappingGroup($order);

        $deliveryId = null;
        foreach ($mappingGroup['deliveries'] ?? [] as $deliveryConfig) {
            if ((empty($deliveryConfig['value']) || $deliveryConfig['value'] == $delivery) && (empty($deliveryConfig['country']) || $deliveryConfig['country'] == $country)) {
                $deliveryId = $deliveryConfig['id_delivery'];
                break;
            }
        }

        $paymentId = null;
        foreach ($mappingGroup['payments'] ?? [] as $paymentConfig) {
            if ((empty($paymentConfig['value']) || $paymentConfig['value'] == $payment) && (empty($paymentConfig['country']) || $paymentConfig['country'] == $country)) {
                $paymentId = $paymentConfig['id_payment'];
                break;
            }
        }

        return $this->findDeliveryType((int) $deliveryId, (int) $paymentId);
    }

    protected function getMappingGroup(\SimpleXMLElement $order): ?array
    {
        $mappingGroup = null;
        $default = null;

        foreach ($this->getConfiguration()['mappingGroups'] ?? [] as $group) {
            // if filter is set, then do check
            if ($filterTag = $order->xpath($group['filter']['tag'] ?? '')[0] ?? null) {
                $filterTagValue = (string) $filterTag;
                $filterValues = array_map('trim', explode(',', $group['filter']['value'] ?? ''));

                if (in_array($filterTagValue, $filterValues)) {
                    $mappingGroup = $group;
                }
            }

            // if the group has no filter set, it is the default group
            if (empty($group['filter']['tag'])) {
                $default = $group;
            }
        }

        return $mappingGroup ?: $default;
    }

    public function in(array $config): void
    {
        $orders = $this->transformXML();

        foreach ($orders->ORDER ?? [] as $xml) {
            [$externalId, $externalData] = $this->getExternalData($xml);
            if (empty($externalId)) {
                $this->addActivityLog(
                    'Nepodařilo se naimportovat objednávku do e-shopu, protože nemá externí ID. V XML souboru chybí "EXTERNAL/ID" element!',
                );
                continue;
            }

            if (!$this->isDropshipOrderValidToImport($xml)) {
                continue;
            }

            // mapping group not found and ignore orders without mapping is enabled, so skip order
            if (($this->getConfiguration()['ignore_on_mapping_not_found'] ?? 'N') === 'Y' && !$this->getMappingGroup($xml)) {
                continue;
            }

            // pokud objednavka uz existuje, tak provedeme pouze aktualizaci
            if ($order = $this->getOrderByExternalId($externalId)) {
                $this->updateDropshipOrder($order, $xml);
                continue;
            }

            try {
                $order = sqlGetConnection()->transactional(function () use ($externalId, $externalData, $xml) {
                    $currencyContext = Contexts::get(CurrencyContext::class);

                    // nactu zakladni data o objednavce pomoci orderImporter servisy
                    $data = $this->orderImporter->getOrderBaseData($xml);

                    // nastavim jazyk objednavky
                    if (findModule(\Modules::TRANSLATIONS)) {
                        $groupSettings = $this->getMappingGroup($xml)['settings'] ?? [];
                        $data['id_language'] = !empty($groupSettings['id_language']) ? $groupSettings['id_language'] : Contexts::get(LanguageContext::class)->getDefaultId();
                    }

                    // pokud neni vyplnena currency, tak nastavim vychozi currency
                    if (empty($data['currency'])) {
                        $data['currency'] = $currencyContext->getDefaultId();
                    }

                    // nactu si informace o mene, pokud mena neexistuje a mam vypnutou prices_to_default_currency, tak vyhazuju chybu
                    $currencyInfo = $this->getCurrencyInfo($data['currency']);

                    // pokud nemam currency rate, tak ho doplnim
                    if (empty($data['currency_rate'])) {
                        $data['currency_rate'] = $currencyInfo->rate;
                    }

                    $noteAdmin = json_decode($data['note_admin'] ?? '', true) ?: [];

                    // najdu a vlozim dopravu k objednavce
                    if ($deliveryType = $this->getDeliveryTypeByConfiguration($xml)) {
                        $data['id_delivery'] = $deliveryType->id;

                        // delivery point - napr.v pripade, ze se jedna o zasilkovnu
                        $deliveryPoint = (string) $xml->DELIVERY_POINT;
                        if (!empty($deliveryPoint) && method_exists($deliveryType->getDelivery(), 'getInfo')) {
                            $noteAdmin['delivery_data'] = $deliveryType->getDelivery()
                                ->setPointId($deliveryPoint)
                                ->getInfo();
                        }
                    }

                    // ceny se budou konvertovat do vychozi meny, takze si pro to pripravim data
                    if ($this->isPriceConvertionEnabled() && $currencyInfo->getCurrencyCode() !== $currencyContext->getDefaultId()) {
                        $data['currency'] = $currencyContext->getDefaultId();
                    }

                    $data['note_admin'] = json_encode($noteAdmin);

                    $order = $this->createDropshipOrder($xml, $data);

                    // obecny feed muze obsahovat i marketplace info, takze v tu chvili chci k objednavce zalogovat o jaky marketplace se jedna
                    $this->logOrderMarketplaceInfo($order, $xml, $externalData);

                    $lastItemTax = \DecimalConstants::zero();
                    foreach ($xml->ITEMS->ITEM as $item) {
                        // zkontroluju, ze jsou vyplneny vsechny povinne udaje pro polozku objednavky
                        if (!$this->checkRequiredXMLData($item, $this->getRequiredOrderItemFields())) {
                            throw new TransferException(
                                sprintf('Nepodařilo se naimportovat objednávku "%s": data položek objednávky nejsou validní', $externalId),
                                (array) $item
                            );
                        }

                        // zkusim najit produkt a variantu
                        [$productId, $variationId] = $this->getProductByItem($item);

                        $isPriceWithVat = ((string) ($item->PIECE_PRICE->attributes()['with_vat'] ?? null)) === 'true';

                        $pieces = toDecimal((string) $item->PIECES);
                        $piecePrice = $this->convertPrice(toDecimal((string) $item->PIECE_PRICE), $currencyInfo);
                        // pokud je cena uvedena s DPH, tak DPH odectu
                        if ($isPriceWithVat) {
                            $piecePrice = $piecePrice->removeVat((string) $item->VAT);
                        }
                        $totalPrice = toDecimal($piecePrice)->mul($pieces);

                        // odecist skladovost produktu
                        if ($productId) {
                            $product = \Variation::createProductOrVariation($productId, $variationId);
                            $product->createFromDB();
                            $product->sell($variationId, $pieces->asFloat());
                        }

                        $itemData = $this->modifyItem(
                            [
                                'id_order' => $order->id,
                                'id_product' => $productId,
                                'id_variation' => $variationId,
                                'pieces' => $pieces,
                                'pieces_reserved' => $pieces,
                                'piece_price' => $piecePrice,
                                'total_price' => $totalPrice,
                                'tax' => (string) $item->VAT,
                                'descr' => (string) $item->NAME,
                                'note' => json_encode(['item_type' => OrderItemInfo::TYPE_PRODUCT]),
                            ],
                            $item
                        );

                        $lastItemTax = toDecimal($itemData['tax']);

                        // vytvorim polozku objednavky
                        sqlQueryBuilder()
                            ->insert('order_items')
                            ->directValues($itemData)
                            ->execute();
                        $itemData['id'] = sqlInsertId();

                        $this->itemCreatedEvent(
                            product: $product ?? null,
                            idVariation: (int) $variationId,
                            piecePrice: $piecePrice,
                            pieces: (int) $pieces->asInteger(),
                            data: [
                                'row' => $itemData,
                                'items_table' => 'order_items',
                            ],
                            order: $order
                        );

                        unset($product);
                    }

                    $deliveryPrice = (string) $xml->DELIVERY_PRICE;
                    $paymentPrice = (string) $xml->PAYMENT_PRICE;

                    $deliveryPrice = !empty($deliveryPrice) ? toDecimal($deliveryPrice) : \DecimalConstants::zero();
                    $paymentPrice = !empty($paymentPrice) ? toDecimal($paymentPrice) : \DecimalConstants::zero();

                    $deliveryPaymentPrice = $this->convertPrice($deliveryPrice->add($paymentPrice), $currencyInfo);

                    // pridani polozky s dopravou a platbou do objednavky
                    if ($deliveryItem = $this->getDeliveryPaymentItem($order, $deliveryType, $deliveryPaymentPrice, $lastItemTax)) {
                        sqlQueryBuilder()
                            ->insert('order_items')
                            ->directValues($deliveryItem)
                            ->execute();
                    }

                    // prepocitam total price objednavky
                    $order->recalculate(round: false);

                    // oznacit objednavku jako zaplacenou, pokud prisel status_payed == 1
                    if (!$order->isPaid() && $data['status_payed'] == 1) {
                        $this->payDropshipOrder($order);
                    }

                    return $order;
                });

                $this->modifyInsertedOrder($order, $xml);
            } catch (\Throwable $e) {
                $this->transferWorker->logException($e, $this);
            }
        }
    }

    public function out(array $config): void
    {
        throw new \RuntimeException('Method "out" is not implemented for generic transfer');
    }

    protected function updateDropshipOrder(\Order $order, \SimpleXMLElement $xml): void
    {
        $data = $this->orderImporter->getOrderBaseData($xml, false);

        $updateData = [];

        if (!empty($data['invoice_dic'])) {
            $updateData['invoice_dic'] = $data['invoice_dic'];
        }
        if (!empty($data['invoice_ico'])) {
            $updateData['invoice_ico'] = $data['invoice_ico'];
        }
        if (!empty($data['delivery_country'])) {
            $updateData['delivery_country'] = trim($data['delivery_country']);
        }
        if (!empty($data['invoice_country'])) {
            $updateData['invoice_country'] = trim($data['invoice_country']);
        }

        // aktualizovat stav zaplaceni
        if (!$order->isPaid() && $data['status_payed'] == 1) {
            $updateData['status_payed'] = 1;
            $this->payDropshipOrder($order);
        }

        if (!empty($updateData)) {
            sqlQueryBuilder()
                ->update('orders')
                ->directValues($updateData)
                ->where(Operator::equals(['id' => $order->id]))
                ->execute();
        }
    }

    protected function getDeliveryPaymentItem(\Order $order, ?\DeliveryType $deliveryType, \Decimal $price, \Decimal $vat): ?array
    {
        $deliveryItemName = 'Doprava a platba';
        if ($deliveryType) {
            $deliveryItemName = $deliveryType->name;
        }

        if (!$price->isPositive()) {
            return null;
        }

        $price = $price->removeVat($vat);

        return [
            'id_order' => $order->id,
            'id_product' => null,
            'id_variation' => null,
            'pieces' => 1,
            'pieces_reserved' => 1,
            'piece_price' => $price,
            'total_price' => $price,
            'descr' => $deliveryItemName,
            'tax' => $vat,
            'note' => json_encode(['item_type' => OrderItemInfo::TYPE_DELIVERY]),
        ];
    }

    protected function transformXML(): ?\SimpleXMLElement
    {
        try {
            if ($xml = $this->loadXML()) {
                if (!empty($this->dropshipment['transformation'])) {
                    $xsl = new \DOMDocument();
                    $xsl->loadXML($this->dropshipment['transformation']);

                    return ErrorHandler::call(fn () => simplexml_import_dom(\AutomaticImportTransform::TransformXml($xsl, $xml)));
                }

                return $xml;
            }
        } catch (\Throwable $e) {
            if (isLocalDevelopment()) {
                throw $e;
            }

            $this->addActivityLog(
                'Nepodařilo se provést transformaci feedu!',
                ['error' => $e->getMessage()]
            );
        }

        return null;
    }

    protected function logOrderMarketplaceInfo(\Order $order, \SimpleXMLElement $xml, array $externalData): void
    {
        // nazev marketplacu, ze ktereho objednavka pochazi
        if (!empty($externalData['marketplace'])) {
            $order->logHistory('[Dropshipment] Marketplace: '.$externalData['marketplace']);
        }

        // marketplace muze mit i nejaky vlastni ID - napr. v pripade baselinkeru do EXTERNAL/ID chodi ID baselinkeru
        // protoze to je to spravne ID pro pripadnou komunikaci s baselinkerem, ale nekdo muze chtit pracovat
        // i primo s ID z daneho marketplacu, takze tady je podpora, aby se pripadne zobrazilo aspon v historii
        if (!empty($externalData['marketplace_id'])) {
            $order->logHistory('[Dropshipment] Marketplace ID: '.$externalData['marketplace_id']);
        }
    }

    protected function getRequiredOrderItemFields(): array
    {
        return [
            'NAME', 'PIECES', 'PIECE_PRICE', 'VAT',
        ];
    }

    private function checkRequiredXMLData(\SimpleXMLElement $xml, array $requiredFields): ?bool
    {
        foreach ($requiredFields as $field) {
            if (!isset($xml->{$field})) {
                return false;
            }
        }

        return true;
    }
}
