<?php

declare(strict_types=1);

namespace External\ZNZBundle\Synchronizers;

use External\ZNZBundle\Exception\ZNZException;
use KupShop\KupShopBundle\Context\CurrencyContext;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\KupShopBundle\Util\Functional\Mapping;
use KupShop\KupShopBundle\Util\StringUtil;
use KupShop\PricelistBundle\Util\PriceListWorker;
use KupShop\QuantityDiscountBundle\Util\QuantityDiscountUtil;
use KupShop\SynchronizationBundle\Exception\RabbitRetryMessageException;
use Query\Operator;

class PriceSynchronizer extends BaseSynchronizer
{
    /** Jedna se o vychozi cenu, ktera se neuklada do ceniku, ale jde rovnou k produktu/variante */
    private const TYPE_DEFAULT = 'default';
    /** Jedna se o vychozi cenu, ktera se uklada rovnou do ceniku (pouze pokud je zapnuto `pricelist_per_website`) */
    private const TYPE_DEFAULT_PRICE_LIST = 'default_price_list';
    /** Jedna se o cenu v ceniku */
    private const TYPE_PRICE_LIST = 'price_list';

    protected static string $type = 'price';

    /** @required */
    public PriceListWorker $priceListWorker;

    /** @required */
    public QuantityDiscountUtil $quantityDiscountUtil;

    public static function getHandledTables(): array
    {
        return [
            'ProduktCena' => 'processPrice',
            'ProduktCenaTier' => 'processQuantityDiscount',
        ];
    }

    public function processPrice(array $item): void
    {
        if ($item['meta']['delete'] ?? false) {
            if (!($item = $this->getProductPriceDeleteItem($item))) {
                return;
            }
        }

        if (!$this->isMessageWithStoreRequiredValid($item)) {
            return;
        }

        // CenovaUroven = ID ceniku v Heliosu, v podstate kazda cena by mela spadat pod nejaky cenik
        if (empty($item['CenovaUroven'])) {
            return;
        }

        if (!($priceListId = $this->znzUtil->getPriceListByZNZId($item['CenovaUroven']))) {
            return;
        }

        [$productId, $variationId] = $this->znzUtil->getProductMapping($item['IdProdukt']);
        if (!$productId) {
            throw new RabbitRetryMessageException('Product not found');
        }

        $variationId = $this->znzUtil->getProductVariationIdWithStoreCheck($productId, $variationId, $item['IdSklad'] ?? null);

        $priceListType = $this->getPriceListType($item, $priceListId);
        $isPriceListWithDefaultPrice = $this->isPriceListWithDefaultPrice($item, $priceListType, $priceListId);

        $maxId = $this->znzUtil->getZNZMaxId($item['IdProdukt'], 'ProduktCena', (string) $item['CenovaUroven']);
        // je to stara zmena, ktera nas uz nezajima, protoze mame novejsi data
        if ($maxId && $maxId > $item['meta']['id_change']) {
            return;
        }

        $price = $this->createProductPrice($productId, $item);

        // aktualizovat vychozi cenu u produktu
        if ($isPriceListWithDefaultPrice) {
            $this->updateItemDefaultPrice($productId, $variationId, $price);
        }

        $this->updatePriceList($priceListId, $productId, $variationId, $price);
        $this->updatePriceForDiscount($priceListId, $productId, $variationId, $price, $isPriceListWithDefaultPrice);

        $this->znzUtil->updateZNZMaxId($item['IdProdukt'], 'ProduktCena', (string) $item['CenovaUroven'], $item['meta']['id_change']);
    }

    public function processQuantityDiscount(array $item): void
    {
        if (!$this->isMessageWithStoreRequiredValid($item)) {
            return;
        }

        [$productId, $variationId] = $this->znzUtil->getProductMapping($item['IDProdukt']);
        if (!$productId) {
            return;
        }

        $variationId = $this->znzUtil->getProductVariationIdWithStoreCheck($productId, $variationId, $item['IdSklad'] ?? null);

        $discount = $this->createProductPrice($productId, $item, 'Sleva4');

        $groups = array_flip($this->quantityDiscountUtil->getGroups());

        // v pripade B2B shopu je mnozstevni sleva per cenik
        if ($this->configuration->isB2BShop()) {
            if (!($priceListId = $this->znzUtil->getPriceListByZNZId($item['CenovaUroven']))) {
                return;
            }

            // id skupiny mnozsteni slevy je stejne jako ID ceniku
            $groupId = $priceListId;
        } else {
            $groupId = null;
            // ulozim si mnozstevni slevu pod spravnou skupinu (skupiny jsou vytvorene podle websites)
            if ($groups[$item['IdWebsite']] ?? false) {
                $groupId = $groups[$item['IdWebsite']];
            }
        }

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

        $currency = $item['Mena4'] ?? $currencyContext->getDefaultId();

        if (!($currencyContext->getAll()[$currency] ?? false)) {
            throw new ZNZException('Currency "'.$currency.'" not found!');
        }

        $this->updateQuantityDiscount(
            $productId,
            $variationId,
            (int) $item['MnozstviOd'],
            $discount,
            $groupId,
            $currency
        );
    }

    protected function getPriceListType(array $item, int $priceListId): string
    {
        // jedna se o cenik s priznakem Vychozi=true
        if ($this->isPriceListDefault($priceListId)) {
            // pokud je zapnuto vychozi cenik pro kazdou website, tak to vzdycky musi byt cenik
            if ($this->configuration->isDefaultPriceListPerWebsite()) {
                return self::TYPE_DEFAULT_PRICE_LIST;
            }

            // pokud je to cenik ve vychozi mene + to je vychozi cenik, tak vracim TYPE_DEFAULT
            // to znamena, ze se cena neulozi do ceniku, ale ulozi se primo k produktu/variante
            if ($item['Mena'] === Contexts::get(CurrencyContext::class)->getDefaultId()) {
                return self::TYPE_DEFAULT;
            }
        }

        return self::TYPE_PRICE_LIST;
    }

    protected function isPriceListDefault(int $priceListId): bool
    {
        static $cache = [];

        if (($cache[$priceListId] ?? null) === null) {
            $cache[$priceListId] = (bool) sqlQueryBuilder()
                ->select('is_default')
                ->from('znz_pricelists')
                ->where(Operator::equals(['id_pricelist' => $priceListId]))
                ->execute()->fetchOne();
        }

        return $cache[$priceListId];
    }

    private function updatePriceForDiscount(int $priceListId, int $productId, ?int $variationId, \Decimal $price, bool $isPriceListWithDefaultPrice): void
    {
        if (!findModule(\Modules::PRICE_HISTORY)) {
            return;
        }

        // aktualizovat CPS v ceniku
        sqlQueryBuilder()
            ->update('pricelists_products')
            ->set('price_for_discount', 'LEAST(:price, price_for_discount)')
            ->setParameter('price', $price)
            ->where(Operator::equalsNullable(['id_pricelist' => $priceListId, 'id_product' => $productId, 'id_variation' => $variationId]))
            ->execute();

        // aktualizovat CPS u produktu/varianty - pokud se jedna o cenik s vychozi cenou
        if ($isPriceListWithDefaultPrice) {
            sqlQueryBuilder()
                ->update($variationId ? 'products_variations' : 'products')
                ->set('price_for_discount', 'LEAST(:price, price_for_discount)')
                ->setParameter('price', $price)
                ->where(Operator::equals(['id' => $variationId ?: $productId]))
                ->execute();
        }
    }

    private function updateItemDefaultPrice(int $productId, ?int $variationId, \Decimal $price): void
    {
        // aktualizovat vychozi cenu
        sqlQueryBuilder()
            ->update($variationId ? 'products_variations' : 'products')
            ->directValues(['price' => $price])
            ->where(Operator::equals(['id' => $variationId ?: $productId]))
            ->execute();
    }

    private function getProductPriceDeleteItem(array $item): ?array
    {
        $parts = explode('-', $item['meta']['unique_id'] ?? '');
        if (empty($parts[0])) {
            return null;
        }

        $znzPriceListId = !empty($parts[2]) ? (int) $parts[2] : null;

        $priceList = $znzPriceListId ? $this->getPriceListByZNZId($znzPriceListId) : null;

        return [
            'IdProdukt' => (int) $parts[0],
            'CenovaUroven' => $znzPriceListId,
            'Mena' => $priceList['currency'] ?? $this->configuration->getWebsiteCurrency($item['meta']['website']),
            'IdWebsite' => $item['meta']['website'],
            'Cena' => 0,
            'meta' => $item['meta'],
        ];
    }

    /**
     * Urcuje, zda se jedna o cenik, ktery zaroven obsahuje vychozi cenu, ktera bude ulozena primo k produktu, aby byla
     * videt v administraci.
     */
    private function isPriceListWithDefaultPrice(array $item, string $priceListType, int $priceListId): bool
    {
        if ($priceListType === self::TYPE_DEFAULT) {
            return true;
        }

        if ($priceListType !== self::TYPE_DEFAULT_PRICE_LIST) {
            return false;
        }

        // pokud neni cenik ve vychozi mene, tak nemuze byt bran jako cenik s vychozi cenou
        if (Contexts::get(CurrencyContext::class)->getDefaultId() !== $this->getPriceListCurrency($priceListId)) {
            return false;
        }

        return $this->configuration->getMainWebsite() === $item['IdWebsite'];
    }

    private function getDefaultPriceListId(string $priceListName, string $currency, array $data = []): int
    {
        static $defaultPriceListsByName;

        if ($defaultPriceListsByName === null) {
            $priceListsAll = sqlQueryBuilder()
                ->select('id, name')
                ->from('pricelists')
                ->execute()->fetchAllAssociative();

            $priceListsByName = Mapping::mapKeys(
                $priceListsAll,
                fn ($k, $v) => [StringUtil::slugify($v['name']), (int) $v['id']]
            );
        }

        // pokud cenik se stejnym nazvem uz existuje, tak vratim jeho ID
        if ($defaultPriceListsByName[StringUtil::slugify($priceListName)] ?? false) {
            return $defaultPriceListsByName[StringUtil::slugify($priceListName)];
        }

        if (!($defaultPriceListsByName[StringUtil::slugify($priceListName)] ?? false)) {
            $priceListId = sqlGetConnection()->transactional(function () use ($priceListName, $currency, $data) {
                $priceListId = $this->priceListWorker->findPriceList($priceListName, $currency);

                sqlQueryBuilder()
                    ->update('pricelists')
                    ->directValues(['data' => json_encode($data)])
                    ->where(Operator::equals(['id' => $priceListId]))
                    ->execute();

                return $priceListId;
            });

            $defaultPriceListsByName[StringUtil::slugify($priceListName)] = $priceListId;
        }

        return $defaultPriceListsByName[StringUtil::slugify($priceListName)];
    }

    private function getPriceListId(int $znzId, string $priceListName, string $currency, array $data = []): int
    {
        static $priceLists, $priceListsByName;

        if ($priceLists === null) {
            $priceListsAll = sqlQueryBuilder()
                ->select('id, name')
                ->from('pricelists')
                ->execute()->fetchAllAssociative();

            $priceLists = Mapping::mapKeys(
                $priceListsAll,
                fn ($k, $v) => [$v['id'], (int) $v['id']]
            );

            $priceListsByName = Mapping::mapKeys(
                $priceListsAll,
                fn ($k, $v) => [StringUtil::slugify($v['name']), (int) $v['id']]
            );
        }

        if ($priceListsByName[StringUtil::slugify($priceListName)] ?? false) {
            return $priceListsByName[StringUtil::slugify($priceListName)];
        }

        if (!($priceLists[$znzId] ?? false)) {
            $priceListId = sqlGetConnection()->transactional(function () use ($znzId, $priceListName, $currency, $data) {
                $tmpId = $this->priceListWorker->findPriceList($priceListName, $currency);

                sqlQueryBuilder()
                    ->update('pricelists')
                    ->directValues(['data' => json_encode($data)])
                    ->where(Operator::equals(['id' => $tmpId]))
                    ->execute();

                if (!empty($znzId) && $tmpId != $znzId) {
                    sqlQueryBuilder()
                        ->update('pricelists')
                        ->directValues(['id' => $znzId])
                        ->where(Operator::equals(['id' => $tmpId]))
                        ->execute();
                }

                return $znzId;
            });

            $priceLists[$znzId] = $priceListId;
        }

        return $priceLists[$znzId];
    }

    private function getPriceListByZNZId(int $znzId): ?array
    {
        static $priceListsById;

        if ($priceListsById === null) {
            $priceListsAll = sqlQueryBuilder()
                ->select('pl.id, pl.name, pl.currency, zpl.id_znz')
                ->from('pricelists', 'pl')
                ->join('pl', 'znz_pricelists', 'zpl', 'zpl.id_pricelist = pl.id')
                ->execute()->fetchAllAssociative();

            $priceLists = Mapping::mapKeys(
                $priceListsAll,
                fn ($k, $v) => [$v['id_znz'], $v]
            );
        }

        return $priceLists[$znzId] ?? null;
    }

    private function updatePriceList(int $priceListId, int $productId, ?int $variationId, \Decimal $price): void
    {
        sqlQueryBuilder()
            ->insert('pricelists_products')
            ->directValues([
                'id_pricelist' => $priceListId,
                'id_product' => $productId,
                'id_variation' => $variationId,
                'price' => $price,
            ])
            ->onDuplicateKeyUpdate(['price'])
            ->execute();
    }

    private function updateQuantityDiscount(int $productId, ?int $variationId, int $pieces, \Decimal $discount, ?int $groupId, string $currency): bool
    {
        $id = sqlQueryBuilder()
            ->select('id')
            ->from('products_quantity_discounts')
            ->where(
                Operator::equalsNullable(
                    [
                        'id_group' => $groupId,
                        'id_product' => $productId,
                        'id_variation' => $variationId,
                        'pieces' => $pieces,
                    ]
                )
            )->execute()->fetchOne();

        if ($id) {
            sqlQueryBuilder()
                ->update('products_quantity_discounts')
                ->directValues(
                    [
                        'discount' => $discount,
                        'discount_type' => $currency,
                    ]
                )
                ->where(Operator::equals(['id' => $id]))
                ->execute();
        } else {
            sqlQueryBuilder()
                ->insert('products_quantity_discounts')
                ->directValues(
                    [
                        'id_group' => $groupId,
                        'id_product' => $productId,
                        'id_variation' => $variationId,
                        'pieces' => $pieces,
                        'discount' => $discount,
                        'discount_type' => $currency,
                    ]
                )->execute();
        }

        return true;
    }

    private function createProductPrice(int $productId, array $item, string $priceField = 'Cena'): \Decimal
    {
        $price = toDecimal($item[$priceField]);

        // pokud je uvedena cena s DPH, tak DPH odeberu aby se to u nas ulozilo spravne
        if (($item['BezDPH'] ?? 'N') === 'N') {
            $price = $price->removeVat(
                $this->znzUtil->getProductVat($productId)
            );
        }

        return $price;
    }

    private function getPriceListCurrency(int $priceListId): string
    {
        static $priceListCurrencyCache = [];

        if (!($priceListCurrencyCache[$priceListId] ?? false)) {
            $priceListCurrencyCache[$priceListId] = sqlQueryBuilder()
                ->select('currency')
                ->from('pricelists')
                ->where(Operator::equals(['id' => $priceListId]))
                ->sendToMaster()
                ->execute()->fetchOne();
        }

        return $priceListCurrencyCache[$priceListId];
    }
}
