<?php

declare(strict_types=1);

namespace External\PompoBundle\DRS\Synchronizer;

use External\PompoBundle\Util\Configuration;
use KupShop\KupShopBundle\Context\VatContext;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\PricelistBundle\Util\PriceListWorker;
use Query\Operator;
use Query\Product;

/**
 * Synchronizace cen.
 */
class PriceSynchronizer extends AbstractSynchronizer
{
    private const FLAG_VIP_CZK = 'VIPCZK';
    private const FLAG_VIP_EUR = 'VIPEUR';

    protected static $type = 'price';

    protected bool $logging = false;

    /** @required */
    public Configuration $configuration;

    /** @var PriceListWorker */
    private $priceListWorker;

    private $lastTimestampGlobal;
    private $lastTimestampLocal;

    /** @required */
    public function setPriceListWorker(PriceListWorker $priceListWorker): void
    {
        $this->priceListWorker = $priceListWorker;
    }

    public function process(): void
    {
        parent::process();

        // Ulozim si posledni timestampy
        if ($this->lastTimestampLocal || $this->lastTimestampGlobal) {
            $dbcfg = \Settings::getDefault();

            $drs = $dbcfg->loadValue('drs') ?? [];
            $drs[static::getType()]['timestamp'] = [
                'local' => $this->lastTimestampLocal,
                'global' => $this->lastTimestampGlobal,
            ];
            $drs[static::getType()]['updated'] = time();

            $dbcfg->saveValue('drs', $drs, false);
        }
    }

    /**
     * Sesynchronizuje ceny konkretniho produktu.
     */
    public function processSingleProductPrices(string $code): void
    {
        foreach ($this->drsApi->getProductPrices($code) as $item) {
            $this->processItem($item);
        }
    }

    /**
     * Sesynchronizuje akcni ceny, u kterych bude zacinat nebo koncit platnost.
     */
    public function processActionPrices(bool $all = false): void
    {
        $date = $this->getTodayDate();

        if ($all) {
            $date = null;
        }

        foreach ($this->drsApi->getPricesStarted($date) as $item) {
            $this->processItem($item);
        }

        foreach ($this->drsApi->getPricesEnded($date) as $item) {
            $this->processItem($item);
        }
    }

    /**
     * Sesynchronizuje cenu produktu.
     */
    protected function processItem(array $item): void
    {
        $this->lastTimestampLocal = max((int) $item['timestampLocal'], (int) $this->lastTimestampLocal);
        $this->lastTimestampGlobal = max((int) $item['timestampGlobal'], (int) $this->lastTimestampGlobal);

        if (!($productId = $this->getProductId($item))) {
            return;
        }

        $productCampaigns = explodeFlags($this->getProductInfo($item)['campaign'] ?? '');
        // pokud ma produkt nastavenou kampan "PF" (Produkt z feedu), tak ignoruju synchronizaci cen s DRSem
        // chci brat v potaz cenu z automatickeho importu
        if (!empty($productCampaigns['PF'])) {
            return;
        }

        // nactu si akcni cenu z lokalnich cen
        $item['discountedPrice'] = $item['localActionPrice'] ?? null;
        $item['actionDateFrom'] = $item['localActionSince'] ?? null;
        $item['actionDateTo'] = $item['localActionTill'] ?? null;

        // pokud nemam akcni cenu v lokalnich cenach, tak to prebije globalni akcni cena
        if (empty($item['localActionPrice']) || !$this->isActionPriceValid($item)) {
            $item['discountedPrice'] = $item['globalActionPrice'] ?? null;
            $item['actionDateFrom'] = $item['globalActionSince'] ?? null;
            $item['actionDateTo'] = $item['globalActionTill'] ?? null;
        }

        switch ($item['currency']) {
            case 'CZK':
                $this->processDefaultPrice($productId, $item);
                break;
            case 'EUR':
                $this->processPriceListPrice($productId, $item);
                break;
        }
    }

    /**
     * Vývhozí cena produktu (Základní ceník) - CZK.
     */
    protected function processDefaultPrice(int $productId, array $item): void
    {
        // nactu si vat produktu
        $vat = $this->getProductVat($productId);

        $price = $this->getPrice($item, (float) $vat);

        // ukladam platnost akcni ceny
        $data = $this->getProductData($item);
        $data['drs']['action'][$item['currency']]['from'] = $item['actionDateFrom'];
        $data['drs']['action'][$item['currency']]['to'] = $item['actionDateTo'];

        // aktualizuju produkt
        sqlQueryBuilder()
            ->update('products')
            ->directValues(
                [
                    'price' => $price,
                    'discount' => $this->getDiscount($item)->printFloatValue(8),
                    'data' => json_encode($data),
                ]
            )
            ->addParameters(
                [
                    'actionFrom' => $item['actionDateFrom'],
                    'actionTo' => $item['actionDateTo'],
                ]
            )
            ->where(Operator::equals(['id' => $productId]))
            ->execute();

        // ulozim cenu pro prihlasene uzivatele - VIP cenik
        if ($this->configuration->getUsersPriceLists()[$item['currency']] ?? false) {
            $this->processUserPriceList(
                $this->configuration->getUsersPriceLists()[$item['currency']],
                $productId,
                $item,
                (float) $vat,
                self::FLAG_VIP_CZK
            );
        }
    }

    /**
     * Zpracuje ceník registrovaných uživatelů - registrovaní uživatelé mají jiné ceny, než neregistrovaní.
     */
    protected function processUserPriceList(int $priceListId, int $productId, array $item, float $vat, ?string $flag = null): void
    {
        // VIP cena registrovanych uzivatelu
        $userPrice = toDecimal((float) $item['userPrice']);

        $price = $this->getPrice($item, $vat);
        $priceWithVat = $price->addVat($vat);
        $discount = $this->getDiscount($item);
        $priceWithVatWithDiscount = $priceWithVat->addDiscount($discount);

        // Odeberu flag
        if ($flag) {
            $this->updateFlag($productId, $flag, true);
        }

        // Pokud je nastavena VIP cena, tak spocitam slevu a tu nasledne ulozim do ceniku
        // V opacnem pripade do ceniku ukladam klasickou slevu, protoze ceniky neumi dedit jiny cenik
        // takze v pripade SK by to s nevyplnenou cenou fallbacknulo na default cenik, kde by byly CZ ceny misto SK cen
        if ($userPrice->isPositive() && $userPrice->lowerThan($priceWithVatWithDiscount)) {
            // Pridam flag pokud produkt ma VIP cenu - kvuli FE, aby slo zobrazovat, ze produkt ma VIP cenu
            if ($flag) {
                $this->updateFlag($productId, $flag);
            }

            // Spocitam VIP slevu
            $discount = $this->calculateDiscount($priceWithVat, $userPrice);
        }

        // Ulozim do ceniku pro registrovane uzivatele
        $this->updatePriceList(
            $priceListId,
            $productId,
            null,
            $price,
            $discount->isPositive() ? ((float) $discount->printFloatValue(8)) : null
        );
    }

    /**
     * Ostatní ceny produktu (ceníky).
     */
    protected function processPriceListPrice(int $productId, array $item): void
    {
        // najdu cenik
        if (!($priceListId = $this->getPriceListId($item))) {
            return;
        }

        // nactu si DPH produktu
        $vat = $this->getProductVat($productId);

        // ulzoim platnost akcni ceny
        $data = $this->getProductData($item);
        $data['drs']['action'][$item['currency']]['from'] = $item['actionDateFrom'];
        $data['drs']['action'][$item['currency']]['to'] = $item['actionDateTo'];

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

        $price = $this->getPrice($item, (float) $vat);

        $discount = $this->getDiscount($item);

        // ukladam cenu do ceniku
        $this->updatePriceList(
            $priceListId,
            $productId,
            null,
            $price,
            $discount->isPositive() ? ((float) $discount->printFloatValue(8)) : null
        );

        // ulozim cenu pro prihlasene uzivatele - VIP cenik
        if ($this->configuration->getUsersPriceLists()[$item['currency']] ?? false) {
            $this->processUserPriceList(
                $this->configuration->getUsersPriceLists()[$item['currency']],
                $productId,
                $item,
                (float) $vat,
                self::FLAG_VIP_EUR
            );
        }
    }

    protected function getItems(): iterable
    {
        $timestamp = [];
        if ($this->mode !== self::MODE_FULL) {
            $dbcfg = \Settings::getDefault();
            $drs = $dbcfg->loadValue('drs');
            $timestamp = $drs[static::getType()]['timestamp'] ?? [];
        }

        // ulozim si aktualni timestampy do memberu
        $this->lastTimestampLocal = $timestamp['local'] ?? null;
        $this->lastTimestampGlobal = $timestamp['global'] ?? null;

        return $this->drsApi->getPrices(
            $timestamp['local'] ?? null,
            $timestamp['global'] ?? null,
        );
    }

    protected function getPriceListId(array $item): ?int
    {
        switch ($item['currency']) {
            case 'EUR':
                return 1;
        }

        return null;
    }

    private function updateFlag(int $productId, string $flag, bool $remove = false): void
    {
        $method = $remove ? 'REMOVE_FROM_SET' : 'ADD_TO_SET';

        sqlQueryBuilder()
            ->update('products')
            ->set('campaign', "{$method}(:flag, campaign)")
            ->setParameter('flag', $flag)
            ->where(Operator::equals(['id' => $productId]))
            ->execute();
    }

    private function getPrice(array $item, float $vat): \Decimal
    {
        $price = toDecimal((float) $item['price']);
        $actionPrice = toDecimal((float) $item['discountedPrice']);

        // pokud je actionPrice vetsi jak price, tak se jako price vezme actionPrice
        if ($price->lowerThan($actionPrice)) {
            $price = $actionPrice;
        }

        return $price->removeVat($vat);
    }

    private function getDiscount(array $item): \Decimal
    {
        $discount = \DecimalConstants::zero();

        $price = toDecimal((float) $item['price']);
        $actionPrice = toDecimal((float) $item['discountedPrice']);
        if ($this->isActionPriceValid($item)) {
            $discount = $this->calculateDiscount($price, $actionPrice);
        }

        return $discount;
    }

    private function calculateDiscount(\Decimal $price, \Decimal $actionPrice): \Decimal
    {
        $discount = \DecimalConstants::zero();
        if ($actionPrice->isPositive() && $actionPrice->lowerThan($price)) {
            $discount = $price->sub($actionPrice)->div($price)->mul(\DecimalConstants::hundred());
        }

        return $discount;
    }

    private function isActionPriceValid(array $item): bool
    {
        $from = $this->getDateTime($item['actionDateFrom']);
        $to = $this->getDateTime($item['actionDateTo']);

        $today = $this->getTodayDate();

        if (($from === null && $to === null) || ($from && $today >= $from) && ($to && $today <= $to)) {
            return true;
        }

        return false;
    }

    private function getTodayDate(): \DateTime
    {
        $date = new \DateTime();
        // pokud je to chvili pred pulnoci, tak uz preskocim na dalsi den :) kvuli cronum, ktery se mohou spustit jeste pred pulnoci
        if ($date->format('H') >= 22) {
            $date = $date->add(new \DateInterval('P1D'));
        }

        return $date;
    }

    private function getProductId(array $item): ?int
    {
        if ($info = $this->getProductInfo($item)) {
            return (int) $info['id'];
        }

        return null;
    }

    private function getProductData(array $item): array
    {
        if ($info = $this->getProductInfo($item, true)) {
            return json_decode($info['data'] ?? '', true) ?? [];
        }

        return [];
    }

    private function getProductVat(int $productId, string $country = 'CZ'): ?float
    {
        // remove static to tmp disable cache bcs of changing vats
        $productsVatCache = [];

        $countries = ['SK'];
        if (!($productsVatCache[$productId] ?? false)) {
            $data = sqlQueryBuilder()
                ->select('p.id, p.vat as defaultVatId')
                ->from('products', 'p')
                ->andWhere(Product::withVats($countries))
                ->andWhere(Operator::equals(['p.id' => $productId]))
                ->execute()->fetchAssociative();

            $defaultVatId = $data['defaultVatId'] ?? null;

            $vatContext = Contexts::get(VatContext::class);

            $productsVatCache[$productId] = $data;
            $productsVatCache[$productId]['vat_CZ'] = $vatContext->getVat($defaultVatId)['vat'] ?? 21;
            foreach ($countries as $vatCountry) {
                if (empty($productsVatCache[$productId]['vat_'.$vatCountry])) {
                    $productsVatCache[$productId]['vat_'.$vatCountry] = $vatContext->getDefault($vatCountry)['vat'];
                }
            }
        }

        return (float) ($productsVatCache[$productId]['vat_'.$country] ?? getVat());
    }

    private function getDateTime(?string $date): ?\DateTime
    {
        if (!$date) {
            return null;
        }

        try {
            $dateTime = new \DateTime($date);
        } catch (\Exception $e) {
            return null;
        }

        return $dateTime;
    }

    private function getProductInfo(array $item, bool $force = false): ?array
    {
        static $productInfo = [];

        if (!($productInfo[$item['code']] ?? false) || $force) {
            $info = sqlGetConnection()->transactional(function () use ($item) {
                return sqlQueryBuilder()
                    ->select('p.id, v.vat, p.data, p.campaign')
                    ->from('products', 'p')
                    ->join('p', 'vats', 'v', 'v.id = p.vat')
                    ->where(
                        Operator::orX(
                            Operator::equalsNullable(['p.code' => $item['code'], 'p.ean' => $item['ean']]),
                            Operator::andX(
                                Operator::equals(['p.code' => $item['code']]),
                                'p.ean IS NULL OR p.ean = 0'
                            )
                        )
                    )
                    ->execute()->fetch();
            });

            if ($info) {
                $productInfo[$item['code']] = $info;
            }
        }

        return $productInfo[$item['code']] ?? null;
    }

    private function updatePriceList(int $priceListId, int $productId, ?int $variationId, \Decimal $price, ?float $discount = null): void
    {
        $this->priceListWorker->updatePricelists(
            $priceListId,
            [
                'price' => $price->asFloat(),
                'discount' => $discount,
                'showVat' => 'N',
            ],
            $productId,
            0,
            $variationId
        );
    }
}
