<?php

namespace KupShop\GraphQLBundle\ApiAdmin\Util;

use KupShop\AdminBundle\Util\Filter\OrdersFilterSpecs;
use KupShop\DropshipBundle\Util\DropshipmentUtil;
use KupShop\GraphQLBundle\ApiAdmin\Types\Collection\OrderCollection;
use KupShop\GraphQLBundle\ApiAdmin\Types\Enums\Order\OrderDocumentType;
use KupShop\GraphQLBundle\ApiAdmin\Types\Order\Input\OrderCreateInput;
use KupShop\GraphQLBundle\ApiAdmin\Types\Order\Input\OrderStornoInput;
use KupShop\GraphQLBundle\ApiAdmin\Types\Order\Input\OrderUpdateInput;
use KupShop\GraphQLBundle\ApiAdmin\Types\Order\Order;
use KupShop\GraphQLBundle\ApiAdmin\Types\Order\OrderDocument;
use KupShop\GraphQLBundle\ApiAdmin\Types\Order\Response\OrderMutateResponse;
use KupShop\GraphQLBundle\ApiAdmin\Types\Parameters;
use KupShop\GraphQLBundle\ApiAdmin\Types\Purchase\Input\PurchaseCalculateInput;
use KupShop\GraphQLBundle\ApiShared\ApiUtil;
use KupShop\GraphQLBundle\Exception\GraphQLNotFoundException;
use KupShop\GraphQLBundle\Exception\GraphQLValidationException;
use KupShop\KupShopBundle\Config;
use KupShop\KupShopBundle\Context\ContextManager;
use KupShop\KupShopBundle\Context\CountryContext;
use KupShop\KupShopBundle\Context\CurrencyContext;
use KupShop\KupShopBundle\Context\LanguageContext;
use KupShop\KupShopBundle\Email\OrderMessageEmail;
use KupShop\KupShopBundle\Exception\UnknownEmailTypeException;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\KupShopBundle\Util\Mail\EmailLocator;
use KupShop\KupShopBundle\Util\ObjectUtil;
use KupShop\KupShopBundle\Util\Price\Price;
use KupShop\OrderingBundle\Entity\Order\OrderItem;
use KupShop\OrderingBundle\Entity\Purchase\PurchaseState;
use KupShop\OrderingBundle\OrderList\OrderList;
use KupShop\OrderingBundle\Util\Order\OrderInfo;
use KupShop\OrderingBundle\Util\Purchase\PurchaseUtil;
use Query\Operator;
use Query\QueryBuilder;

class OrderUtil
{
    /** @var OrderList */
    private $orderList;

    public function __construct(
        private readonly OrdersFilterSpecs $ordersFilterSpecs,
        private readonly EmailLocator $emailLocator,
        private readonly \KupShop\GraphQLBundle\ApiAdmin\Util\PurchaseUtil $graphqlPurchaseUtil,
        private readonly PurchaseUtil $purchaseUtil,
        private readonly ContextManager $contextManager,
        private readonly ?DropshipmentUtil $dropshipmentUtil,
    ) {
    }

    public function getOrder(int $id): ?Order
    {
        $orderList = $this->getOrderList();
        $orderList->andSpec(Operator::equals(['o.id' => $id]));

        $orders = $orderList->getOrders();

        if (!($order = $orders->current())) {
            throw new GraphQLNotFoundException('Order not found');
        }

        $order->productList->fetchPhotos('product_gallery', ['Y', 'N']);

        return new Order($order, $orders);
    }

    public function getOrders(?int $offset, int $limit, ?Parameters $sort, ?Parameters $filter): OrderCollection
    {
        $sortValues = $sort ? array_filter($sort->getData()) : [];
        $filterValues = $filter ? array_filter($filter->getData()) : [];

        $orderList = clone $this->orderList;

        // limit
        $orderList->limit(ApiUtil::getLimit($limit), $offset);

        // sorting
        foreach ($sortValues as $field => $sortValue) {
            $orderList->andSpec(function (QueryBuilder $qb) use ($field, $sortValue) {
                $qb->addOrderBy($field, $sortValue->value);
            });
        }

        if ($specs = $this->getOrdersFilterSpecs($filterValues)) {
            $orderList->andSpec(Operator::andX($specs));
        }

        return (new OrderCollection($orderList->getOrders($totalCount)))
                ->setItemsTotalCount($totalCount ?? 0)
                ->setLimit($limit)
                ->setOffset($offset);
    }

    public function stornoOrder(OrderStornoInput $input): OrderMutateResponse
    {
        try {
            $order = \Order::get($input->id);
        } catch (\InvalidArgumentException $e) {
            throw new GraphQLNotFoundException($e->getMessage());
        }

        if ($order->status_storno == 1) {
            throw new GraphQLValidationException('Cannot cancel order! Order is already cancelled.');
        }

        $stornoHistoryMessage = '[API] Stornování objednávky.';
        if ($input->message) {
            $stornoHistoryMessage .= 'Důvod: '.$input->message;
        }

        $order->logHistory($stornoHistoryMessage);
        $order->storno(false, $input->message, $input->sendMail);

        return new OrderMutateResponse(true, $this->getOrder($input->id));
    }

    public function updateOrder(OrderUpdateInput $input): OrderMutateResponse
    {
        try {
            $order = \Order::get($input->id);
        } catch (\InvalidArgumentException $e) {
            throw new GraphQLNotFoundException($e->getMessage());
        }

        $updateLog = [];

        // aktualizace polozek objednavky
        if (ObjectUtil::isPropertyInitialized($input, 'items')) {
            foreach ($input->items ?: [] as $item) {
                if (!ObjectUtil::isPropertyInitialized($item, 'id') || empty($item->id)) {
                    throw new GraphQLValidationException(
                        'OrderUpdateInput.items.id: Item ID is mandatory when updating order items!'
                    );
                }

                /** @var OrderItem $orderItem */
                $orderItem = $order->getItems()[$item->id] ?? false;

                if (!$orderItem) {
                    throw new GraphQLValidationException(
                        sprintf('OrderUpdateInput.items: Item with ID "%s" is not included in the order!', $item->id)
                    );
                }

                if (ObjectUtil::isPropertyInitialized($item, 'delete')) {
                    if ($item->delete === true) {
                        $order->deleteItem($item->id);
                        $updateLog[] = "Smazání položky \"{$item->id}\": {$orderItem->getDescr()}; {$orderItem->getPieces()} ks";
                        continue;
                    }
                }

                if (ObjectUtil::isPropertyInitialized($item, 'quantity')) {
                    if ($item->quantity < 0) {
                        throw new GraphQLValidationException(
                            'OrderUpdateInput.items.quantity: Quantity cannot be negative value!'
                        );
                    }

                    // aktualizace poctu kusu u polozky
                    if ($orderItem->getPieces() != $item->quantity) {
                        $order->updateItem($item->id, $item->quantity);
                        $updateLog[] = "Aktualizace položky \"{$item->id}\": {$orderItem->getDescr()}; {$orderItem->getPieces()} -> {$item->quantity} ks";
                    }
                }
            }

            // smazani items cache, aby se pripadne polozky prenacetly podle aktualniho stavu
            $order->items = [];
        }

        // aktualizace cisla baliku
        if (ObjectUtil::isPropertyInitialized($input, 'packageId')) {
            $order->setPackageId($input->packageId);
            $updateLog[] = "Nastaveno číslo balíku: {$input->packageId}";
        }

        // custom faktura - TODO: odebrat az si to na sartoru zmeni
        if (ObjectUtil::isPropertyInitialized($input, 'invoiceUrl') && !ObjectUtil::isPropertyInitialized($input, 'documents')) {
            $document = new OrderDocument();
            $document->type = OrderDocumentType::from('order_invoice');
            $document->url = $input->invoiceUrl;
            $input->documents = [$document];
        }

        // custom doklady
        if (ObjectUtil::isPropertyInitialized($input, 'documents')) {
            $externalDocuments = $order->getData('externalDocuments') ?: [];
            foreach ($input->documents ?: [] as $document) {
                $externalDocuments[$document->type->getValue()] = $document->url;
                $updateLog[] = sprintf('Nastaven doklad <strong>%s</strong>: <a href="%2$s" target="_blank">%2$s</a>', $document->type->getValue(), $document->url);
            }
            $order->setData('externalDocuments', $externalDocuments);
        }

        if (ObjectUtil::isPropertyInitialized($input, 'userId')) {
            if (!$this->checkUserExists($input->userId)) {
                throw new GraphQLNotFoundException(sprintf('User with ID %s was not found!', $input->userId));
            }

            $order->assignUser($input->userId);
        }

        // zmena stavu objednavky
        if (ObjectUtil::isPropertyInitialized($input, 'status') && $input->status) {
            if (!(getOrderStatuses()[$input->status->id] ?? false)) {
                throw new GraphQLNotFoundException(
                    sprintf('Status with ID "%s" was not found!', $input->status->id)
                );
            }

            if ($input->status->comment) {
                $order->logHistory($input->status->comment);
            }

            if ($input->status->emailType) {
                try {
                    $this->emailLocator->getEmailService($input->status->emailType);
                } catch (UnknownEmailTypeException) {
                    throw new GraphQLValidationException(
                        sprintf('Email type "%s" does not exists!', $input->status->emailType)
                    );
                }
            }

            if (!$input->status->userMessage) {
                $input->status->userMessage = $this->getOrderMessageByStatus($input->status->id);
            }

            if (ObjectUtil::isPropertyInitialized($input->status, 'sendEmail')) {
                $sendEmail = $input->status->sendEmail;
            }

            $order->changeStatus(
                $input->status->id,
                null,
                $sendEmail ?? null,
                $input->status->userMessage,
                $input->status->emailType,
            );
        }

        // zaloguju info o zmenach, ktere byly behem API volani provedeny
        if (!empty($updateLog)) {
            $order->logHistory('<strong>[API] Byly provedeny následující aktualizace:</strong><br>'.implode('<br>', array_map(fn ($x) => "- {$x}", $updateLog)));
        }

        return new OrderMutateResponse(true, $this->getOrder($input->id));
    }

    private function getOrderMessageByStatus(int $statusId): ?string
    {
        $email = sqlQueryBuilder()
            ->select('name')
            ->from('emails')
            ->where(
                Operator::equals(
                    [
                        'enabled' => 'Y',
                        'type' => OrderMessageEmail::getType(),
                        'order_status' => $statusId,
                    ]
                )
            )->execute()->fetchOne();

        if ($email) {
            return $email;
        }

        return null;
    }

    public function getOrdersFilterSpecs(array $filterValues): ?callable
    {
        $specs = [];
        $filter = [];

        if ($filterValues['id'] ?? false) {
            $specs[] = Operator::inIntArray((array) $filterValues['id'], 'o.id');
        }

        if ($filterValues['code'] ?? false) {
            $codeFilter = (array) $filterValues['code'];

            if (count($codeFilter) === 1) {
                $specs[] = Operator::equals(['o.order_no' => reset($codeFilter)]);
            } else {
                $specs[] = Operator::inIntArray((array) $filterValues['code'], 'o.order_no');
            }
        }

        if (!empty($filterValues['dateCreated'])) {
            $specs[] = ApiUtil::getDateTimeFilter($filterValues['dateCreated'], 'o.date_created');
        }

        if (!empty($filterValues['dateUpdated'])) {
            $specs[] = ApiUtil::getDateTimeFilter($filterValues['dateUpdated'], 'o.date_updated');
        }

        if ($filterValues['status'] ?? false) {
            $filter['statuses'] = (array) $filterValues['status'];
        }

        if ($filterValues['userId'] ?? false) {
            $filter['reg_users'] = (array) $filterValues['userId'];
        }

        if ($filterValues['deliveryId'] ?? false) {
            $filter['deliveries'] = (array) $filterValues['deliveryId'];
        }

        if ($filterValues['paymentId'] ?? false) {
            $filter['payments'] = (array) $filterValues['paymentId'];
        }

        if ($filterValues['sellerId'] ?? false) {
            $filter['sellers'] = (array) $filterValues['sellerId'];
        }

        if ($filterValues['source'] ?? false) {
            $filter['source'] = (array) $filterValues['source'];
        }

        if ($filterValues['dropshipId'] ?? false) {
            $filter['dropship'] = (array) $filterValues['dropshipId'];
        }

        // deprecated - just backward compatibility
        if (($filterValues['date_created']['from'] ?? false) || ($filterValues['date_created']['to'] ?? false)) {
            $from = ApiUtil::prepareDateTimeForDB($filterValues['date_created']['from'] ?? null);
            $to = ApiUtil::prepareDateTimeForDB($filterValues['date_created']['to'] ?? null);

            $specs[] = Operator::between(
                'o.date_created',
                new \Range($from, $to)
            );
        }

        return $this->ordersFilterSpecs->getSpecs($filter, $specs);
    }

    private function getOrderList(): OrderList
    {
        return clone $this->orderList;
    }

    /**
     * @required
     */
    public function setOrderList(OrderList $orderList): void
    {
        $this->orderList = $orderList;
    }

    public function createOrder(OrderCreateInput $orderCreateInput, array $fetchOptions = []): OrderMutateResponse
    {
        $this->validateOrderCreateInput($orderCreateInput);

        return sqlGetConnection()->transactional(function () use ($orderCreateInput, $fetchOptions) {
            return $this->contextManager->activateContexts([CountryContext::class => $orderCreateInput->deliveryAddress?->country ?? $orderCreateInput->invoiceAddress->country],
                function () use ($orderCreateInput, $fetchOptions) {
                    $purchaseCalculateInput = new PurchaseCalculateInput();
                    $purchaseCalculateInput->items = $orderCreateInput->items;
                    $purchaseCalculateInput->coupons = $orderCreateInput->coupons ?? [];

                    $purchaseStateInfo = $this->graphqlPurchaseUtil->calculatePurchaseState($purchaseCalculateInput);
                    $purchaseState = $purchaseStateInfo->getPurchaseState() ?? new PurchaseState([]);

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

                    $flags = array_intersect($orderCreateInput->flags ?? [], array_keys(Config::get()['Order']['Flags']));

                    $orderData = [
                        'source' => OrderInfo::ORDER_SOURCE_API,

                        'note_invoice' => $orderCreateInput->invoice->note ?? null,

                        'date_created' => ApiUtil::prepareDateTimeForDB($orderCreateInput->dateCreated ?: new \DateTime()),
                        'date_accept' => ApiUtil::prepareDateTimeForDB($orderCreateInput->dateAccept) ?: null,
                        'date_handle' => ApiUtil::prepareDateTimeForDB($orderCreateInput->dateHandle) ?: null,
                        'date_delivered' => ApiUtil::prepareDateTimeForDB($orderCreateInput->dateDelivered) ?: null,
                        'date_due' => ApiUtil::prepareDateTimeForDB($orderCreateInput->dateDue) ?: null,

                        'status' => $orderCreateInput->status ?: 0,

                        'id_user' => $orderCreateInput->userId ?? null,

                        // invoice
                        'invoice_email' => $orderCreateInput->invoiceAddress->email,
                        'invoice_name' => $orderCreateInput->invoiceAddress->name,
                        'invoice_surname' => $orderCreateInput->invoiceAddress->surname,
                        'invoice_phone' => $orderCreateInput->invoiceAddress->phone ?? '',
                        'invoice_firm' => $orderCreateInput->invoiceAddress->firm ?? '',
                        'invoice_ico' => $orderCreateInput->invoiceAddress->ico ?? '',
                        'invoice_dic' => $orderCreateInput->invoiceAddress->dic ?? '',
                        'invoice_street' => $orderCreateInput->invoiceAddress->street ?? '',
                        'invoice_city' => $orderCreateInput->invoiceAddress->city ?? '',
                        'invoice_zip' => $orderCreateInput->invoiceAddress->zip ?? '',
                        'invoice_custom_address' => $orderCreateInput->invoiceAddress->customAddress ?? '',
                        'invoice_state' => $orderCreateInput->invoiceAddress->state ?? '',
                        'invoice_country' => $orderCreateInput->invoiceAddress->country ?? '',

                        // delivery
                        'delivery_name' => $orderCreateInput->deliveryAddress->name ?? $orderCreateInput->invoiceAddress->name ?? '',
                        'delivery_surname' => $orderCreateInput->deliveryAddress->surname ?? $orderCreateInput->invoiceAddress->surname ?? '',
                        'delivery_firm' => $orderCreateInput->deliveryAddress->firm ?? $orderCreateInput->invoiceAddress->firm ?? '',
                        'delivery_street' => $orderCreateInput->deliveryAddress->street ?? $orderCreateInput->invoiceAddress->street ?? '',
                        'delivery_city' => $orderCreateInput->deliveryAddress->city ?? $orderCreateInput->invoiceAddress->city ?? '',
                        'delivery_zip' => $orderCreateInput->deliveryAddress->zip ?? $orderCreateInput->invoiceAddress->zip ?? '',
                        'delivery_country' => $orderCreateInput->deliveryAddress->country ?? $orderCreateInput->invoiceAddress->country ?? '',
                        'delivery_state' => $orderCreateInput->deliveryAddress->state ?? $orderCreateInput->invoiceAddress->state ?? '',
                        'delivery_custom_address' => $orderCreateInput->deliveryAddress->customAddress ?? $orderCreateInput->invoiceAddress->customAddress ?? '',
                        'delivery_phone' => $orderCreateInput->deliveryAddress->phone ?? $orderCreateInput->invoiceAddress->phone ?? '',
                        'delivery_email' => $orderCreateInput->deliveryAddress->email ?? '',

                        'flags' => implode(',', $flags),

                        'note_user' => $orderCreateInput->noteUser ?? '',
                    ];

                    $customData = [];
                    foreach ($orderCreateInput->data ?? [] as $data) {
                        $customData[$data->key] = $data->value;
                    }

                    $orderData['note_admin'] = json_encode($customData);

                    if (findModule(\Modules::INVOICES) && $orderCreateInput->invoice && ObjectUtil::isPropertyInitialized($orderCreateInput->invoice, 'code')) {
                        $orderData['invoice_no'] = $orderCreateInput->invoice->code ?? null;
                    }

                    if (findModule(\Modules::CURRENCIES)) {
                        $orderData['id_language'] = $orderCreateInput->language ?? $languageContext->getDefaultId();
                        $orderData['currency'] = $orderCreateInput->currency ?? $currencyContext->getDefaultId();
                        $orderData['currency_rate'] = $orderCreateInput->currencyRate ?? $currencyContext->getOrDefault($orderData['currency'])->getRate();
                    }

                    $purchaseState->setSource(OrderInfo::ORDER_SOURCE_API);

                    if ($deliveryId = $orderCreateInput->deliveryType?->id ?? false) {
                        $deliveryType = \DeliveryType::get($deliveryId, true);

                        $purchaseState->setDeliveryType($deliveryType);

                        if (($deliveryPrice = $orderCreateInput->deliveryType?->deliveryPrice) !== null) {
                            $deliveryVat = $deliveryType->getPrice()->getVat()->asFloat();
                            $price = new Price(toDecimal($deliveryPrice)->removeVat($deliveryVat), $currencyContext->getOrDefault($orderData['currency']), $deliveryVat);

                            $purchaseState->setDeliveryPrice($price);
                        }
                    }

                    $purchaseState->setCustomData(
                        [
                            'order' => $orderData,
                        ]
                    );

                    $purchaseState = $this->purchaseUtil->recalculateTotalPrices($purchaseState);

                    $order = $this->purchaseUtil->createOrderFromPurchaseState(
                        purchaseState: $purchaseState,
                        useDeliveryFromPurchaseState: true,
                        onCreatedCallback: fn (int $orderId) => $this->insertOrderDropshipment($orderId, $orderCreateInput)
                    );

                    $order->logHistory('[API] Objednávka vznikla přes API');
                    // log user note to order so it is visible in history
                    if ($orderCreateInput->noteUser) {
                        $order->logHistory($orderCreateInput->noteUser);
                    }

                    return new OrderMutateResponse(true, $this->getOrder($order->id, $fetchOptions));
                });
        });
    }

    protected function validateOrderCreateInput(OrderCreateInput $input): void
    {
        if ($this->dropshipmentUtil) {
            if (!$this->dropshipmentUtil->getDropshipment($input->dropshipment->id)) {
                throw new GraphQLValidationException('OrderCreateInput.dropshipment.id: Given dropshipment ID does not exist!');
            }

            if (empty($input->dropshipment->externalId)) {
                throw new GraphQLValidationException('OrderCreateInput.dropshipment.externalId: Order externalId cannot be empty!');
            }

            $dropshipmentOrderExists = sqlQueryBuilder()
                ->select('id_order')
                ->from('order_dropshipment')
                ->where(Operator::equals(['id_external' => $input->dropshipment->externalId]))
                ->execute()->fetchOne();

            if ($dropshipmentOrderExists) {
                throw new GraphQLValidationException(
                    sprintf('OrderCreateInput.dropshipment.externalId: Order with this external ID already exists!')
                );
            }
        }

        if ($input->status && !$this->checkOrderStatus($input->status)) {
            throw new GraphQLValidationException('Specified order status is not valid. Use configuration.orderStatuses to get available statuses.');
        }

        if ($input->invoice) {
            if (ObjectUtil::isPropertyInitialized($input->invoice, 'code') && !empty($input->invoice->code) && $this->checkInvoiceExists($input->invoice->code)) {
                throw new GraphQLValidationException(sprintf('Invoice with this code "%s" already exists', $input->invoice->code));
            }
        }

        if ($input->language && !$this->checkLanguageValid($input->language)) {
            throw new GraphQLValidationException(sprintf('Language "%s" is not supported.', $input->language));
        }

        if ($input->currency && !$this->checkCurrencyValid($input->currency)) {
            throw new GraphQLValidationException(sprintf('Currency "%s" is not supported', $input->currency));
        }

        if ($input->deliveryType?->id && !$this->checkDeliveryTypeValid($input->deliveryType->id)) {
            throw new GraphQLValidationException('Specified delivery type is not valid. Use configuration.deliveryTypes to get available delivery types.');
        }

        if ($input->userId && !$this->checkUserExists($input->userId)) {
            throw new GraphQLValidationException(sprintf('User with this ID "%s" does not exist', $input->userId));
        }
    }

    protected function insertOrderDropshipment(int $orderId, OrderCreateInput $input): void
    {
        $dropshipmentData = [];
        foreach ($input->dropshipment->data ?? [] as $data) {
            $dropshipmentData[$data->key] = $data->value;
        }

        $dropshipment = [
            'id_order' => $orderId,
            'id_dropshipment' => $input->dropshipment->id,
            'id_external' => $input->dropshipment->externalId,
            'data' => json_encode($dropshipmentData),
        ];

        sqlQueryBuilder()
            ->insert('order_dropshipment')
            ->directValues($dropshipment)
            ->execute();
    }

    public function checkOrderStatus(int $orderStatus): bool
    {
        return in_array($orderStatus, array_keys(getOrderStatuses()));
    }

    public function checkInvoiceExists(?string $code): bool
    {
        return sqlQueryBuilder()
            ->select('id')
            ->from('orders')
            ->where(Operator::equals(['invoice_no' => $code]))
            ->execute()->fetchOne();
    }

    public function checkLanguageValid(string $language): bool
    {
        return isset(Contexts::get(LanguageContext::class)->getAll()[$language]);
    }

    public function checkCurrencyValid(string $currency): bool
    {
        $currencyContext = Contexts::get(CurrencyContext::class);

        return $currencyContext->isValid($currency);
    }

    public function checkDeliveryTypeValid(int $deliveryId): bool
    {
        return \DeliveryType::get($deliveryId, true)->id > 0;
    }

    public function checkUserExists(int $userId): bool
    {
        return sqlQueryBuilder()
            ->select('id')
            ->from('users')
            ->where(Operator::equals(['id' => $userId]))
            ->execute()->fetchOne();
    }
}
