<?php

namespace KupShop\OrderingBundle\Util\Order;

use KupShop\KupShopBundle\Context\ContextManager;
use KupShop\KupShopBundle\Context\CurrencyContext;
use KupShop\KupShopBundle\Query\JsonOperator;
use KupShop\KupShopBundle\Util\Price\PriceCalculator;
use KupShop\OrderingBundle\Cart;
use KupShop\OrderingBundle\Event\OrderEvent;
use Query\Operator;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

class OrderUtil
{
    use \DatabaseCommunication;

    /**
     * @var ContextManager
     */
    private $contextManager;
    /**
     * @var CurrencyContext
     */
    private $currencyContext;

    private EventDispatcherInterface $eventDispatcher;

    public function __construct(ContextManager $contextManager, CurrencyContext $currencyContext, EventDispatcherInterface $eventDispatcher)
    {
        $this->contextManager = $contextManager;
        $this->currencyContext = $currencyContext;
        $this->eventDispatcher = $eventDispatcher;
    }

    public function copyOrder($oldOrderId, $additionalExcept = []): \Order
    {
        $oldOrder = sqlQueryBuilder()->select('*')
            ->from('orders')
            ->andWhere(Operator::equals(['id' => $oldOrderId]))
            ->execute()->fetch();

        return sqlGetConnection()->transactional(function () use ($oldOrder, $additionalExcept) {
            $oldOrder['order_no'] = '_'.rand(0, 999999);
            $except = array_merge([
                'id', 'invoice_no', 'date_created', 'date_accept', 'date_handle',
                'date_delivered', 'date_updated', 'date_due', 'pohoda_id', 'package_id',
            ], $additionalExcept);

            $note = json_decode($oldOrder['note_admin'], true) ?? [];
            foreach ($note as $key => $_) {
                if (!in_array($key, ['delivery_data', 'payment_data'])) {
                    unset($note[$key]);
                }
            }

            $oldOrder['note_admin'] = json_encode($note);

            $this->insertSQL('orders', $oldOrder, $except);

            $ID = sqlInsertId();

            sqlQuery('UPDATE orders SET date_created=NOW() WHERE id=:id', ['id' => $ID]);

            $newOrder = new \Order();
            $newOrder->createFromDB($ID);

            // dispatch order created event - order no is generated in event subscriber
            $this->eventDispatcher->dispatch(new OrderEvent($newOrder), OrderEvent::ORDER_CREATED);

            return $newOrder;
        });
    }

    public function removeItemVat(\Order $order, $id_item, $keepFinalPrice = false)
    {
        $this->contextManager->activateOrder($order, function () use ($keepFinalPrice, $order, $id_item) {
            $item = sqlQueryBuilder()->select('oi.id, oi.piece_price, oi.pieces, oi.tax')
                ->from('order_items as oi')
                ->where(Operator::equals(['oi.id' => $id_item]))
                ->execute()
                ->fetch();
            if ($item) {
                $this->updateItemVat($order, $item, 0, $keepFinalPrice);
            }
        });
    }

    public function removeVat(\Order $order, $vat_rate = 0, $onlyProducts = false, $keepFinalPrice = false)
    {
        $this->contextManager->activateOrder($order, function () use ($keepFinalPrice, $onlyProducts, $order, $vat_rate) {
            $items = sqlQueryBuilder()->select('id, piece_price, pieces, tax, id_product')
                ->from('order_items')
                ->where(Operator::equals(['id_order' => $order->id]))
                ->execute();

            foreach ($items as $item) {
                if ($onlyProducts && !$item['id_product']) {
                    continue;
                }

                $this->updateItemVat($order, $item, $vat_rate, $keepFinalPrice);
            }

            $order->recalculate();
        });
    }

    public function updateItemVat(\Order $order, $item, $vat_rate, $keepFinalPrice = false)
    {
        if ($item['tax'] == $vat_rate) {
            return;
        }

        if ($keepFinalPrice) {
            $newPrice = toDecimal($item['piece_price'])->addVat($item['tax'])->removeVat($vat_rate);
        } else {
            $newPrice = roundPrice(toDecimal($item['piece_price'])->addVat($vat_rate))->removeVat($vat_rate);
        }

        sqlQueryBuilder()->update('order_items')
            ->directValues([
                'tax' => $vat_rate,
                'piece_price' => $newPrice->asFloat(),
                'total_price' => $newPrice->mul(toDecimal($item['pieces'])),
            ])
            ->andWhere(Operator::equals(['id_order' => $order->id, 'id' => $item['id']]))
            ->execute();
    }

    public function addDiscount(\Order $order, \Decimal $discount, $roundPrice = true)
    {
        $this->contextManager->activateOrder($order, function () use ($order, $discount, $roundPrice) {
            $items = sqlQueryBuilder()->select('id', 'piece_price', 'pieces', 'tax', 'discount', 'id_product')
                ->from('order_items')
                ->where(Operator::equals(['id_order' => $order->id]))
                ->execute();

            foreach ($items as $item) {
                if (empty($item['id_product'])) {
                    continue;
                }

                $oldPrice = $piecePrice = toDecimal($item['piece_price']);
                if ($piecePrice->isZero()) {
                    continue;
                }

                $piecePrice = $piecePrice->addVat($item['tax']);
                $piecePrice = $piecePrice->addDiscount($discount);
                if ($roundPrice) {
                    $piecePrice = roundPrice($piecePrice);
                }
                $piecePrice = $piecePrice->removeVat($item['tax']);

                $oldPrice = $oldPrice->removeDiscount($item['discount']);
                $percValue = $oldPrice->div(toDecimal(100));
                $newDiscount = $oldPrice->sub($piecePrice)->div($percValue);

                sqlQueryBuilder()->update('order_items')
                    ->directValues([
                        'piece_price' => $piecePrice,
                        'total_price' => $piecePrice->mul(toDecimal($item['pieces'])),
                        'discount' => $newDiscount,
                    ])
                    ->where(Operator::equals(['id_order' => $order->id, 'id' => $item['id']]))
                    ->execute();
            }

            $order->recalculate();
        });
    }

    public function addFlag(\Order $order, $flag)
    {
        $result = sqlQueryBuilder()->update('orders')
            ->set('flags', 'ADD_TO_SET(:flag, flags)')
            ->setParameter('flag', $flag)
            ->where(Operator::equals(['id' => $order->id]))
            ->execute();

        // pridam ten flag i na Order object, protoze jinak by se tam neobjevil dokud se objednavka znovu nenacte z DB
        if ($result && !($order->getFlags()[$flag] ?? false)) {
            $order->flags = implode(',', array_filter([$order->flags, $flag]));
        }

        return $result;
    }

    public function removeFlag(\Order $order, $flag)
    {
        return sqlQueryBuilder()->update('orders')
            ->set('flags', 'REMOVE_FROM_SET(:flag, flags)')
            ->setParameter('flag', $flag)
            ->where(Operator::equals(['id' => $order->id]))
            ->execute();
    }

    public function updatePriceBuy(\Order $order)
    {
        $qb = sqlQueryBuilder()->update('order_items', 'oi')
            ->join('oi', 'products', 'p', 'p.id = oi.id_product')
            ->joinVariationsOnProducts()
            ->andWhere(Operator::equals(['oi.id_order' => $order->id]))
            ->andWhere(Operator::equalsToOrNullable('oi.id_variation', 'pv.id'))
            ->andWhere('oi.price_buy IS NULL AND COALESCE(pv.price_buy, p.price_buy) IS NOT NULL')
            ->set('oi.price_buy', 'COALESCE(pv.price_buy, p.price_buy) / :currency_rate')
            ->setParameter('currency_rate', $order->currency_rate);
        $res = $qb->execute();

        if ($order->getDeliveryId()) {
            $deliveryItem = sqlQueryBuilder()->select('id')
                ->from('order_items', 'oi')
                ->andWhere(Operator::equals(['oi.id_order' => $order->id]))
                ->andWhere('oi.id_product IS NULL')
                ->andWhere(Operator::equals([JsonOperator::value('oi.note', 'item_type') => 'delivery']))
                ->andWhere('oi.price_buy IS NULL')
                ->forUpdate()
                ->execute()->fetchOne();
            if ($deliveryItem) {
                if ($price_buy = $order->getDeliveryType()->getPriceBuy()) {
                    $price_buy = PriceCalculator::convert($price_buy, $this->currencyContext->getOrDefault($order->currency));
                    sqlQueryBuilder()->update('order_items', 'oi')
                        ->andWhere(Operator::equals(['oi.id' => $deliveryItem]))
                        ->set('oi.price_buy', $price_buy->getValue())
                        ->execute();
                }
            }
        }
    }

    /**
     * Inserts items of order to specified cart.
     */
    public function orderReorder(\Order $order, Cart $cart): void
    {
        foreach ($order->items as $item) {
            if (empty($item['id_product']) || $item['pieces'] <= 0) {
                continue;
            }

            if ($item['product']['visible'] != 'Y') {
                addUserMessage(str_replace('{PRODUCT}', $item['descr'], translate('reorder_out_of_stock', 'order')), 'error');
                continue;
            }
            // přeskakuji položky, které byly vytvořeny rozpadem sety
            if (isset($item['note']['set_info']['id_parent_item'])) {
                continue;
            }

            $item['note'] = array_filter($item['note'] ?? [], fn ($k) => !in_array($k, $this->getOrderReorderIgnoredNoteFields()), ARRAY_FILTER_USE_KEY);
            $add_item = array_filter($cart->filterFields($item, ['id_product', 'id_variation', 'pieces', 'note']));

            $cart->addItem($add_item);
        }
    }

    protected function getOrderReorderIgnoredNoteFields(): array
    {
        return ['set_info', 'priceWithoutDiscounts', 'totalDiscount', 'totalDiscountPerc', 'discounts', 'set_has_been_split'];
    }

    public function clearItemsCache(\Order $order)
    {
        $order->items = [];
    }

    public function updateOrderRate(\Order $order)
    {
        if (!$order->id) {
            return false;
        }

        $rate = \CNB::getCurrency($order->getCurrency());

        return sqlQueryBuilder()
            ->update('orders')
            ->set('currency_rate', $rate)
            ->where(Operator::equals(['id' => $order->id]))
            ->execute();
    }
}
