<?php

namespace KupShop\OrderingBundle\EventListener;

use KupShop\I18nBundle\Util\PriceConverter;
use KupShop\KupShopBundle\Context\CurrencyContext;
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
use KupShop\KupShopBundle\Util\Price\Price;
use KupShop\KupShopBundle\Util\Price\PriceCalculator;
use KupShop\KupShopBundle\Util\Price\PriceLevelPrice;
use KupShop\KupShopBundle\Util\Price\ProductPrice;
use KupShop\OrderDiscountBundle\Util\DiscountUtil;
use KupShop\OrderingBundle\Entity\Purchase\ProductPurchaseItem;
use KupShop\OrderingBundle\Event\OrderItemEvent;
use KupShop\OrderingBundle\Event\OrderItemInfoEvent;
use Query\Operator;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class OrderItemListener implements EventSubscriberInterface
{
    use \DatabaseCommunication;

    private $currencyContext;

    public function __construct(CurrencyContext $currencyContext)
    {
        $this->currencyContext = $currencyContext;
    }

    /** @var PriceConverter */
    protected $priceConverter;

    /**
     * @required
     */
    public function setPriceConverter(PriceConverter $priceConverter)
    {
        $this->priceConverter = $priceConverter;
    }

    /**
     * @var DiscountUtil
     */
    protected $discountUtil;

    /**
     * @required
     */
    public function setDiscountUtil(?DiscountUtil $discountUtil)
    {
        $this->discountUtil = $discountUtil;
    }

    protected const SET_HAS_BEEN_SPLIT = 'set_has_been_split';
    private array $itemRowCache = [];

    public static function getSubscribedEvents()
    {
        $events = [
            OrderItemEvent::ITEM_CREATED => [
                ['SplitSetItem', 200],
            ],
        ];

        if (findModule('products_sets', 'calculate_price')) {
            $events[OrderItemEvent::CALCULATE_PRICE] = [
                ['calculateSetsPrice', 200],
            ];
        }

        $events[OrderItemInfoEvent::INFO] = [
            ['getItemInfo', 200],
        ];

        return $events;
    }

    public function getItemInfo(OrderItemInfoEvent $event)
    {
        $item = $event->getOrderItem();
        $product = $event->getProduct();

        if ($product && findModule(\Modules::PRODUCTS_PARAMETERS, \Modules::SUB_CONFIGURATIONS)) {
            $this->getProductNoteInfo($event, $product);
        }

        if ($product && findModule(\Modules::PRODUCTS, \Modules::SUB_RECYCLING_FEE)) {
            $this->getRecyclingFeeInfo($event, $product);
        }

        $this->getItemDiscountsInfo($event, $item);
    }

    public function getProductNoteInfo(OrderItemInfoEvent $event, \Product $product)
    {
        if (!empty($note = $product->printNote($event->getData()))) {
            $event->addInfoLine('configurations', $note);
        }
    }

    public function getRecyclingFeeInfo(OrderItemInfoEvent $event, \Product $product)
    {
        if ($fee = $product->getData()['recycling_fee'] ?? null) {
            $fee = toDecimal($fee);
            if (!$fee->isZero()) {
                $fee = $this->priceConverter->convert($this->currencyContext->getDefault(), $this->currencyContext->getActive(), $fee);
                $fee = printPrice($fee, ['ceil' => false]);
                $recycling_fee_note = translate('recycling_fee', 'products', false, true).': '.$fee;
                $event->addInfoLine('recycling_fee', $recycling_fee_note);
            }
        }
    }

    public function getItemDiscountsInfo(OrderItemInfoEvent $event, $item)
    {
        $custom_data = $event->getData();
        if ($discounts = $custom_data['discounts'] ?? null) {
            $parts = [];
            $vat = $item['vat'] ?? $item['tax'] ?? 0;
            $pieces = toDecimal($item['pieces']);
            $admin = (isAdministration() || isSuperuser() || $event->isForceAdmin());
            foreach ($discounts as $discount) {
                $discountPrice = toDecimal($discount['discountPrice'])->mul($pieces)->addVat($vat);
                $printPrice = printPrice($discountPrice, ['ceil' => false, 'decimal' => 'dynamic']);
                if ($discount['unit'] == 'perc') {
                    $parts[] = $discount['discount'].'%'.($admin ? " = {$printPrice}" : '');
                } else {
                    $parts[] = $printPrice;
                }
            }
            $priceWithoutDiscounts = printPrice(toDecimal($custom_data['priceWithoutDiscounts'])->mul($pieces), ['ceil' => false, 'decimal' => 'dynamic']);
            $totalDiscount = printPrice(toDecimal($custom_data['totalDiscount'])->mul($pieces), ['ceil' => false, 'decimal' => 'dynamic']);
            $totalDiscountPerc = printPrice($custom_data['totalDiscountPerc'], ['ceil' => false, 'decimal' => 'dynamic', 'printcurrency' => false]);
            $note = translate_shop('priceWithoutDiscounts', 'ordering')." {$priceWithoutDiscounts}, ";
            $note .= translate_shop('totalDiscount', 'ordering')." {$totalDiscount}";
            if ($admin) {
                $note .= " (-{$totalDiscountPerc} %)*";
                $event->addInfoLine('discounts', $note);
                $note_admin = '['.translate_shop('discounts', 'ordering').': '.join('; ', $parts).']';
                $event->addInfoLine('discounts_admin', $note_admin);
                $event->addInfoLine('event_data', [
                    'priceWithoutDiscounts' => $priceWithoutDiscounts,
                    'totalDiscount' => $totalDiscount,
                    'totalDiscountPerc' => $totalDiscountPerc,
                ]);
            } else {
                if (count($parts) > 1) {
                    $note .= ' ('.join(' + ', $parts).')*';
                }
                $event->addInfoLine('discounts', $note);
            }
        }
    }

    public function SplitSetItem(OrderItemEvent $event)
    {
        if (!findModule(\Modules::PRODUCT_SETS)) {
            return;
        }

        // e.g. cloned order - items were already split.
        // Just copy the items from the old order and do not run any logic.
        ['items_table' => $tableName, 'row' => $row] = $event->getData();
        $itemRow = $this->getItemRow($tableName, (int) $row['id']);
        if (!$this->shouldSplitSetItem($itemRow)) {
            return;
        }

        $dbcfg = \Settings::getDefault();
        $splitType = $dbcfg['products_sets']['split'] ?? 'price_vat';

        // mam vypnuty rozpad setu, chci ho zachovat jako samostatny produkt
        if ($splitType === 'no_split') {
            return;
        }

        // price_vat = produkty za puvodni cenu a sleva v samostatne polozce
        // price_no_discount - vubec neuvadet slevu, rozpocitat na zaklade vah
        if ($splitType == 'price_vat' || $splitType == 'price_no_discount') {
            $this->SplitSetItemPriceVat($event);
        } else {
            // celkova cena za set a produkty za 0 Kc
            $this->SplitSetItemDefault($event);
        }
    }

    protected function SplitSetItemDefault(OrderItemEvent $event)
    {
        $product = $event->getProduct();
        $product->fetchSets();

        if (!$product->sets) {
            return;
        }

        $data = $event->getData();
        $items_table = $data['items_table'];
        $row = $data['row'];

        $orderTotalPrice = $event->getPrice();
        $decimalPieces = toDecimal($row['pieces']);

        $itemRow = $this->getItemRow($items_table, (int) $row['id']);

        $note = $itemRow['note'] ?? [];

        $item_note = ['set_info' => ['id_parent_item' => $row['id'], 'id_set_product' => $product->id]];

        if ($calculate_price = findModule(\Modules::PRODUCT_SETS, \Modules::SUB_PRODUCT_SETS_CALCULATE_PRICE)) {
            // celkova cena setu s DPH
            $setProductPrice = toDecimal($itemRow['total_price'])->addVat($itemRow['tax'])->round(2);
            // pocet polozek v objednavce = pocet produktu v setu + 1 hlavni produkt
            $setProductsCount = toDecimal(count($product->sets) + 1);
            // celkova cena 1 polozky setu s DPH
            $orderItemTotalPrice = $setProductPrice->div($setProductsCount, 2);
            $item_note = array_merge($item_note, ['item_type' => 'product']);
        } else {
            $orderItemTotalPrice = \DecimalConstants::zero();
            $item_note = array_merge($item_note, ['item_type' => 'gift']);
        }

        $str = translate_shop('code', 'order');

        /** @var \Product $set_product */
        foreach ($product->sets as $set_product) {
            if (!$this->checkSetProductCustomCondition($set_product, $event)) {
                continue;
            }

            $variation = $this->checkSetVariations($set_product, $note);

            $title = $this->setModifyTitle($set_product, $variation['title'] ?? false);

            // pridat kod zbozi na konec
            if (empty($cfg['Order']['hideCode']) && $set_product['code'] != '') {
                $title .= str_replace('%CODE', trim($set_product['code']), $str);
            }

            $setPieces = toDecimal($set_product['set_pieces'])->mul($decimalPieces);
            $set_product->sell($variation['id'], $setPieces->asFloat());
            $piece_price = $orderItemTotalPrice->div($setPieces, 2);
            $piece_price = $piece_price->removeVat($set_product->vat)->round(\Decimal::$default_scale);
            $total_price = $piece_price->mul($setPieces, \Decimal::$default_scale);

            $insertValues = [
                'id_order' => $itemRow['id_order'],
                'id_product' => $set_product->id,
                'id_variation' => $variation['id'] ?? null,
                'pieces' => $setPieces,
                'pieces_reserved' => $setPieces,
                'piece_price' => $piece_price,
                'total_price' => $total_price,
                'descr' => $title,
                'tax' => $set_product->vat,
                'note' => json_encode($item_note),
                'date' => new \DateTime(),
            ];

            $this->insertSetItem($items_table, $insertValues, $note);

            if ($calculate_price) {
                $total_price_with_vat = $total_price->addVat($set_product->vat)->round(2);
                $setProductPrice = $setProductPrice->sub($total_price_with_vat, 2);
            }
        }

        $updates = [];
        if ($calculate_price) {
            $orderItemTotalPrice = $setProductPrice;
            $piece_price = $orderItemTotalPrice->div($decimalPieces, 2);
            $piece_price = $piece_price->removeVat($itemRow['tax'])->round(\Decimal::$default_scale);
            $total_price = $piece_price->mul($decimalPieces, \Decimal::$default_scale);

            $updates = [
                'piece_price' => $piece_price,
                'total_price' => $total_price,
            ];
        }

        $splitKey = self::SET_HAS_BEEN_SPLIT;
        $updateQuery = sqlQueryBuilder()
            ->update($items_table)
            ->set('note', "JSON_SET(note, '$.{$splitKey}', 'true')")
            ->andWhere(Operator::equals(['id' => $itemRow['id']]));

        if ($updates) {
            $updateQuery->directValues($updates);
        }

        $updateQuery->execute();
        $this->clearItemRowCache($items_table, (int) $itemRow['id']);
    }

    protected function SplitSetItemPriceVat(OrderItemEvent $event)
    {
        $dbcfg = \Settings::getDefault();

        $product = $event->getProduct();
        $product->fetchSets();

        // TODO: tmp logging kvuli tiande - obcas se stane, ze se nerozpadne set, ale netusime proc
        $logger = ServiceContainer::getService('logger');
        $logger->notice(
            'SplitSet: Objednavka '.$event->getOrder()->order_no,
            [
                'orderId' => $event->getOrder()->id,
                'product' => $product->id,
                'productLoadedSets' => $product->sets,
                'itemData' => $event->getData(),
            ],
        );

        if (!$product->sets) {
            return;
        }

        $data = $event->getData();
        $items_table = $data['items_table'];
        $row = $data['row'];

        $decimalPieces = toDecimal($row['pieces']);

        $itemRow = $this->getItemRow($items_table, (int) $row['id']);

        $note = $itemRow['note'] ?? [];

        // tmp logging - stale jsou na tiande objednavky, kde se nerozpadne set a stale nevime proc
        $logger->notice(
            'Rozpad setu - po selectu itemRow: '.$event->getOrder()->order_no,
            [
                'orderId' => $event->getOrder()->id,
                'product' => $product->id,
                'productSets' => $product->sets,
                'itemRow' => $itemRow,
                'note' => $note,
            ]
        );

        $activeCurrency = $this->currencyContext->getActive();
        $productsPrice = array_map(function ($item) use ($decimalPieces, $activeCurrency) {
            /** @var \Product $item */
            $quantity = toDecimal($item['set_pieces'])->mul($decimalPieces);
            $productPrice = PriceCalculator::convert($item->getProductPrice(), $activeCurrency);

            return $productPrice->getPriceWithVat()->mul($quantity)->asFloat();
        }, $product->sets);

        $sumVat = toDecimal(array_sum($productsPrice));
        $productPriceWithVat = roundPrice(toDecimal($itemRow['piece_price'])->addVat($itemRow['tax']));
        if (!empty($note['priceWithoutDiscounts'])) {
            // na produkt byla uplatnena sleva s rozpočítáváním k jednotlivým produktům,
            // ktera se nesmi pocitat do slevy za set
            $productPriceWithVat = toDecimal($note['priceWithoutDiscounts']);
        }
        if ($dbcfg['products_sets']['split'] == 'price_no_discount') {
            $priceRatio = $productPriceWithVat->mul($decimalPieces)->div($sumVat);
        }
        $discount = \DecimalConstants::zero();
        if (!$sumVat->isZero()) {
            $discount = $productPriceWithVat->mul($decimalPieces)
                ->div($sumVat)->sub(\DecimalConstants::one());
        }

        /** @var \Product $set_product */
        foreach ($product->sets as $set_product) {
            $variation = $this->checkSetVariations($set_product, $note);

            $title = $this->setModifyTitle($set_product, $variation['title'] ?? false);

            $price = PriceCalculator::convert($set_product->getProductPrice(), $activeCurrency)->getPriceWithoutVat();
            if ($dbcfg['products_sets']['split'] == 'price_no_discount') {
                $price = $price->mul($priceRatio);
            }

            // id_parent_item is always null, because parent item will be deleted
            $item_note = ['set_info' => ['id_parent_item' => null, 'id_set_product' => $product->id]];
            if (!empty($note['discounts'])) {
                // na produkt byla uplatnena sleva s rozpočítáváním k jednotlivým produktům
                $priceWithoutDiscounts = new ProductPrice($price, $activeCurrency, $set_product['vat']);
                $productPurchaseItem = new ProductPurchaseItem($set_product->id, $variation['id'] ?? null, \DecimalConstants::one(), $priceWithoutDiscounts, $note);
                $totalDiscount = \DecimalConstants::zero();
                foreach ($note['discounts'] as $note_discount) {
                    $discountPrice = $this->discountUtil->divideDiscountPrice([$productPurchaseItem], $note_discount);
                    $totalDiscount = $totalDiscount->add($discountPrice);
                    $discounts = $productPurchaseItem->getDiscounts();
                    $discounts[array_key_last($discounts)]['name'] = $note_discount['name'];
                }
                $item_note['priceWithoutDiscounts'] = $priceWithoutDiscounts->getPriceWithVat(false)->round(2);
                $item_note['totalDiscount'] = $totalDiscount;
                $priceWithDiscounts = $item_note['priceWithoutDiscounts']->sub($totalDiscount);
                $item_note['totalDiscountPerc'] = \DecimalConstants::one()
                    ->sub($priceWithDiscounts->div($item_note['priceWithoutDiscounts']))
                    ->mul(\DecimalConstants::hundred())->round(\Decimal::$default_scale);
                $item_note['discounts'] = $discounts;
                $price = $priceWithDiscounts->removeVat($set_product['vat']);
            }

            $setPieces = toDecimal($set_product['set_pieces'])->mul($decimalPieces);
            $TotalPrice = $price->mul($setPieces);
            $sumPriceByVat[$set_product['vat']] = ($sumPriceByVat[$set_product['vat']] ?? \DecimalConstants::zero())->add($TotalPrice);

            $set_product->sell($variation['id'] ?? false, $setPieces->asInteger());

            $insertValues = [
                'id_order' => $itemRow['id_order'],
                'id_product' => $set_product->id,
                'id_variation' => $variation['id'] ?? null,
                'pieces' => $setPieces,
                'pieces_reserved' => $setPieces,
                'piece_price' => $price,
                'total_price' => $TotalPrice,
                'descr' => $title,
                'tax' => $set_product->vat,
                'date' => new \DateTime(),
                'note' => json_encode($item_note),
            ];

            // tmp logging - stale jsou na tiande objednavky, kde se nerozpadne set a stale nevime proc
            $logger->notice(
                'Rozpad setu - pred insertem: '.$event->getOrder()->order_no,
                [
                    'orderId' => $event->getOrder()->id,
                    'set_product' => [$set_product][0],
                    'itemInsertValues' => $insertValues,
                ]
            );

            $lastInsertId = $this->insertSetItem($items_table, $insertValues, $note);
            $logger->notice(
                'Rozpad setu - po insertu: '.$event->getOrder()->order_no,
                [
                    'orderId' => $event->getOrder()->id,
                    'lastInsertId' => $lastInsertId,
                ]
            );
        }
        $affectedRowsCount = $this->deleteSQL($items_table, ['id' => $itemRow['id']]);
        $this->clearItemRowCache($items_table, (int) $itemRow['id']);

        $logger->notice(
            'Rozpad setu - po deletu: '.$event->getOrder()->order_no,
            [
                'orderId' => $event->getOrder()->id,
                'itemRow' => $itemRow['id'] ?? '',
                'row' => $row['id'] ?? '',
                'affectedRows' => $affectedRowsCount,
                'items_table' => $items_table,
            ]
        );

        if (!$discount->value(4)->isZero() && $dbcfg['products_sets']['split'] != 'price_no_discount') {
            foreach ($sumPriceByVat ?? [] as $vat => $vatSum) {
                $dphTotalDiscount = $discount->mul($vatSum);
                $descr = translate_shop('discount_for_set', 'ordering');
                if (count($sumPriceByVat) > 1) {
                    $descr .= ' '.$vat.'%';
                }
                $insertValues = [
                    'id_order' => $itemRow['id_order'],
                    'id_product' => null,
                    'id_variation' => null,
                    'pieces' => $decimalPieces,
                    'pieces_reserved' => $decimalPieces,
                    'piece_price' => $dphTotalDiscount->div($decimalPieces),
                    'total_price' => $dphTotalDiscount,
                    'descr' => $descr.' - '.$itemRow['descr'],
                    'note' => json_encode(['item_type' => 'discount']),
                    'tax' => $vat,
                    'date' => new \DateTime(),
                ];
                $this->insertSetItem($items_table, $insertValues, $note);
            }
        }
    }

    public function checkSetVariations($set_product, $note)
    {
        $variation = ['title' => '', 'id' => null];

        if ($set_product->hasVariations()) {
            $set_product->fetchVariations();
            $set_variation = $note['set_variation'][$set_product->id] ?? null;
            // v note - varianta vybrana zakaznikem na frontendu
            if (is_array($set_variation)) {
                $set_variation = $set_variation[$set_product['set_id']] ?? null;
            }
            if ($set_variation > 0) {
                $variation['id'] = $set_variation;
            } elseif (($set_product['selected_variation'] ?? null) > 0) {
                // selected_variation - v nastaveni setu je vybrana konkretni varianta
                $variation['id'] = $set_product['selected_variation'];
            } else {
                // varianta neni vybrana na FE (a neni predvybrana v nastaveni setu) - napr. pri pridavani z adminu
                $variation['id'] = $set_product['variationId'] ?? null;
            }
            if (empty($variation['id'])) {
                return false;
            }
            $varianta = $set_product->findVariation($variation['id']);
            if (!$varianta) {
                return false;
            }

            $variation['title'] = $varianta['title'];
        }

        return $variation;
    }

    protected function setModifyTitle($set_product, $variation)
    {
        $productTitle = $set_product->title ?? ($set_product->title_raw ?? $set_product);

        if ($variation) {
            $productTitle .= ' '.$variation;
        }

        return $productTitle;
    }

    public function calculateSetsPrice(OrderItemEvent $event)
    {
        if (!($calculate_price = findModule(\Modules::PRODUCT_SETS, \Modules::SUB_PRODUCT_SETS_CALCULATE_PRICE))) {
            return;
        }

        // cena setu = zacina na nule a postupne se k ni prictou ceny polozek v setu
        $price = \DecimalConstants::zero();

        // TODO: tenhle pradlo-elegant hnus by se uz pomalu mohl presunout na shop. Je jen potreba tuhle fci nejak predefinovat na shopu
        // TODO: ale jinak fetchSets by na to uz mel byt ready diky presunuti do servisy SetsPriceCalculator
        if ($calculate_price === 'pradlo-elegant') {
            // cena setu = cena hlavniho produktu + soucet cen setovych produktu (pradlo-elegant)
            $price = $event->getPrice();
        }

        $product = $event->getProduct();
        $data = $event->getData();

        $masterProductPrice = clone $product->getProductPrice();

        if (!($sets = $product->fetchSets())) {
            return;
        }

        if ($calculate_price === 'pradlo-elegant') {
            // TODO: BIG UGLY HACK pridat discount na price pred prictenim ceny setoveho produktu
            $price = $price->addDiscount($product->discount);
        }

        // cena, ke ktere se budou pricitat ceny setovych polozek
        $price = new Price($price, $this->currencyContext->getDefault(), $product->vat);

        /** @var \Product $set */
        foreach ($sets as $set) {
            $productPrice = $set->getProductPrice();

            if ($set_variation = ($data['set_variation'][$set['id']] ?? null)) {
                if (is_array($set_variation)) {
                    $set_variation = $set_variation[$set['set_id']] ?? null;
                }
                if ($set_variation) {
                    $set->fetchVariations();
                    if ($variation = $set->findVariation($set_variation)) {
                        // pokud je u setove polozky predvyplnena konkretni cena, tak pouziju tu (ulozena v productPrice na set produktu)
                        // pokud vyplnena neni, tak pouziju normalne cenu od varianty
                        $productPrice = !empty($set->set_price_raw) ? $productPrice : $variation['productPrice'];
                    }
                }
            }

            $setPrice = new Price($productPrice->getPriceWithoutVat()->mul(toDecimal($set['set_pieces'])), $productPrice->getCurrency(), $productPrice->getVat());

            $price = PriceCalculator::add($price, $setPrice);
        }

        $price = $price->getPriceWithoutVat();
        if ($calculate_price === 'pradlo-elegant') {
            // TODO: BIG UGLY HACK odebrat prictenou disocunt
            $price = $price->removeDiscount($product->discount);
        }

        // hnus ja vim... ale pokud scitam ceny produktu, tak tam uz je aplikovana cenova hladina
        // a v KupShop\OrderingBundle\Util\Purchase\PurchaseUtil::getItemPrice se aplikuje znova, takze je ta cena
        // ponizena znova, a to nechci
        if ($masterProductPrice instanceof PriceLevelPrice) {
            $priceLevelDiscount = $masterProductPrice->getPricelevelDiscount();
            if ($priceLevelDiscount['unit'] === 'perc' && $priceLevelDiscount['discount'] > 0) {
                $price = $price->removeDiscount(toDecimal($priceLevelDiscount['discount']));
            }
        }

        $event->setPrice($price);
    }

    public function insertSetItem(string $items_table, array $data, ?array $setNote = []): int
    {
        $this->insertSQL($items_table, $data, [], ['date' => 'datetime']);

        return sqlInsertId();
    }

    protected function checkSetProductCustomCondition(\Product $setProduct, OrderItemEvent $event): bool
    {
        return true;
    }

    protected function getItemRow(string $tableName, int $itemId): array
    {
        // used only for parseNote
        static $product = new \Product();

        if (isset($this->itemRowCache[$tableName][$itemId])) {
            return $this->itemRowCache[$tableName][$itemId];
        }

        $query = sqlQueryBuilder()->select('*')
            ->from($tableName)
            ->andWhere(Operator::equals(['id' => $itemId]));

        $row = $query->execute()->fetchAssociative();
        $row['note'] = $product->parseNote($row['note']);

        return $this->itemRowCache[$tableName][$itemId] = $row;
    }

    protected function clearItemRowCache(?string $tableName = null, ?int $id = null): void
    {
        if (isset($tableName)) {
            if (isset($id)) {
                unset($this->itemRowCache[$tableName][$id]);
            } else {
                unset($this->itemRowCache[$tableName]);
            }

            return;
        }

        $this->itemRowCache = [];
    }

    private function shouldSplitSetItem(array $orderItem): bool
    {
        return empty($orderItem['note'][self::SET_HAS_BEEN_SPLIT]);
    }
}
