<?php

declare(strict_types=1);

namespace External\PompoBundle\DataGo\Synchronizer;

use External\PompoBundle\DataGo\Util\PhotoDownloader;
use External\PompoBundle\DRS\Synchronizer\StockSynchronizer as DRSStockSynchronizer;
use KupShop\CatalogBundle\Parameters\ParameterFinder;
use KupShop\CatalogBundle\Product\ProductFinder;
use KupShop\I18nBundle\Translations\ProductsTranslation;
use KupShop\KupShopBundle\Util\Functional\Mapping;
use KupShop\OSSVatsBundle\Util\VatsUtil;
use KupShop\PricelistBundle\Util\PriceListWorker;
use Query\Operator;

class ProductSynchronizer extends AbstractSynchronizer
{
    // Import ze souboru, kde jsou vsechny produkty (pouze zaklada nove produkty)
    public const PROCESS_TYPE_FULL = 'full';
    // Aktualizuje data ze souboru se vsema produktama
    public const PROCESS_TYPE_FULL_UPDATE = 'fullUpdate';
    // Import ze souboru, kde jsou jen nove produkty za poslednich nekolik hodin
    public const PROCESS_TYPE_NEW = 'new';
    // Import ze souboru, kde jsou jen zmeny za poslednich nekolik hodin
    public const PROCESS_TYPE_DIFF = 'diff';

    private const IGNORED_STATUSES = ['1010'];

    protected static $type = 'product';

    /** @required */
    public DRSStockSynchronizer $drsStockSynchronizer;

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

    /** @required */
    public VatsUtil $vatsUtil;

    private $updateMode = false;

    private string $processType = self::PROCESS_TYPE_FULL;

    private array $createdItems = [];

    /** @var ProductFinder */
    private $productFinder;

    /** @var ParameterFinder */
    private $parameterFinder;

    /** @var ProductsTranslation */
    private $productsTranslation;

    /** @var PhotoDownloader */
    private $photoDownloader;

    /** @required */
    public function setProductFinder(ProductFinder $productFinder): void
    {
        $this->productFinder = $productFinder;
    }

    /** @required */
    public function setParameterFinder(ParameterFinder $parameterFinder): void
    {
        $this->parameterFinder = $parameterFinder;
    }

    /** @required */
    public function setProductsTranslation(ProductsTranslation $productsTranslation)
    {
        $this->productsTranslation = $productsTranslation;
    }

    /** @required */
    public function setPhotoDownloader(PhotoDownloader $photoDownloader): void
    {
        $this->photoDownloader = $photoDownloader;
    }

    public function setProcessType(string $processType): self
    {
        $this->processType = $processType;

        return $this;
    }

    public function setUpdateMode(bool $updateMode): self
    {
        $this->updateMode = $updateMode;

        return $this;
    }

    public function process(): void
    {
        $this->processType($this->processType);

        if ($this->configuration->isPompo()) {
            // update DRS (prodejny) stock of created items
            if (!empty($this->createdItems)) {
                $this->drsStockSynchronizer->syncItemsByCode($this->createdItems);
            }
        }
    }

    public function processType(string $type): void
    {
        // nactu si file hash, ktery si ukladam do DB, abych zbytecne nesynchronizoval stejny soubor vickrat
        $hash = $this->dataGoApi->getCSVFileHash(
            $this->getCSVFileNameByType($type)
        );

        $lastHash = $this->getLastFileHashByType($type);
        // synchronizujeme jen pokud se file hash zmenil
        if ($lastHash && ($lastHash === $hash)) {
            return;
        }

        $items = $this->dataGoApi->fetchCSVData(
            $this->getCSVFileNameByType($type)
        );

        // sesynchronizovat
        foreach ($items as $item) {
            switch ($type) {
                case self::PROCESS_TYPE_FULL_UPDATE:
                case self::PROCESS_TYPE_DIFF:
                    $this->processItemUpdate($item);
                    break;
                default:
                    $this->processItem($item);
                    break;
            }
        }

        $this->setLastFileHashByType($type, $hash);
    }

    protected function processItemUpdate(array $item): void
    {
        if (!$this->isItemValid($item)) {
            return;
        }

        // chceme produkty pouze aktualizovat
        if (!($productId = $this->productFinder->getProductIdByCode($item['cislo-zbozi']))) {
            return;
        }

        $vat = $this->getItemVat($item);

        // odetekuju zmenu DPH
        [$isVatChanged, $oldVat] = $this->isProductVatChanged($productId, $vat);

        // find OSS category
        if ($CNKey = ($item['celni-cislo'] ?? null)) {
            $CNKey = $this->vatsUtil->getCNKey($CNKey);
        }

        $update = [
            'vat' => $this->productFinder->findVAT($vat->asFloat()),
            'price_buy' => $item['nc-cena-bez-dph'],
            'id_cn' => $CNKey,
        ];

        sqlQueryBuilder()
            ->update('products')
            // smazu kampan "Produkt z feedu" od produktu, pokud produkt prijde z DataGo
            ->set('campaign', 'REMOVE_FROM_SET(:campaign, campaign)')
            ->directValues($update)
            ->where(Operator::equals(['id' => $productId]))
            ->setParameter('campaign', 'PF')
            ->execute();

        $this->processParameters($productId, $item, [
            'Typ produktu',
            'Název dodavatele',
            'Objednací číslo u dodavatele',
            'Celní číslo',
            'Celní preference',
            'Status typ',
            'EANs',
        ]);

        $this->processPrices($productId, $item);

        if ($isVatChanged) {
            $this->updatePriceForNewVat($productId, $oldVat, $vat->asFloat());
        }
    }

    protected function processItem(array $item): void
    {
        if (!$this->isItemValid($item)) {
            return;
        }

        // chceme produkty pouze zakladat
        if (($productExists = $this->productFinder->getProductIdByCode($item['cislo-zbozi'])) && !$this->updateMode) {
            return;
        }

        // najdu, pripadne vytvorim produkt
        if (!($productId = ((int) $this->productFinder->findProduct($item['cislo-zbozi'])))) {
            return;
        }

        $vat = $this->getItemVat($item);

        $price = toDecimal($item['pc-cena-s-dph']);
        $price = $price->removeVat($vat);

        // find OSS category
        if ($CNKey = ($item['celni-cislo'] ?? null)) {
            $CNKey = $this->vatsUtil->getCNKey($CNKey);
        }

        $update = [
            'title' => $item['nazev-zbozi'],
            'ean' => $item['ean'],
            'vat' => $this->productFinder->findVAT($vat->asFloat()),
            'price' => $price,
            'price_buy' => $item['nc-cena-bez-dph'],
            'producer' => $this->getProducer($item['vyrobce']),
            'long_descr' => nl2br($item['popis-zbozi-cz']),
            'width' => $item['kus-sirka-v-cm'],
            'height' => $item['kus-vyska-v-cm'],
            'depth' => $item['kus-hloubka-v-cm'],
            'weight' => $item['hmotnost-jednoho-kusu-jednotky-zbozi'],
            'id_cn' => $CNKey,
        ];

        // pokud je to novy produkt
        if (!$productExists) {
            $update['figure'] = 'N';
            $this->createdItems[] = $item['cislo-zbozi'];
        }

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

        $this->processTranslations($productId, $item);
        $this->processParameters($productId, $item);
        $this->processProductPhotos($productId, $item['cislo-zbozi']);
        $this->processPrices($productId, $item);
    }

    public function processPrices(int $productId, array $item): void
    {
        $dmocPriceLists = $this->configuration->getDMOCPriceLists();

        $vat = $this->getItemVat($item);

        // ulozit cenu do ceniku "DMOC - CZK"
        if ($dmocPriceLists['CZK'] ?? false) {
            $this->priceListWorker->updatePriceList(
                (int) $dmocPriceLists['CZK'],
                $productId,
                null,
                toDecimal($item['doporucena-moc-bez-dph-v-kc'])
            );
        }

        // ulozit cenu do ceniku "DMOC - EUR"
        if ($dmocPriceLists['EUR'] ?? false) {
            $this->priceListWorker->updatePriceList(
                (int) $dmocPriceLists['EUR'],
                $productId,
                null,
                toDecimal($item['doporucena-moc-s-dph-v-eur'])->removeVat($vat)
            );
        }

        // VIP cenik je jen na pompu
        if ($this->configuration->isPompo()) {
            $price = toDecimal($item['nc-cena-bez-dph']);
            if ($price->isPositive()) {
                // nakupni cena + 5%
                $price = $price->add($price->div(\DecimalConstants::hundred())->mul(toDecimal(5)));
            }

            // ulozit cenu do ceniku "Nákupní cena + 5%"
            $this->priceListWorker->updatePriceList(
                $this->configuration->getVIPPriceListId(),
                $productId,
                null,
                $price
            );
        }
    }

    public function processParameters(int $productId, array $item, ?array $parameterName = null): void
    {
        foreach ($this->getParameterFields() as $field => $parameter) {
            if ($parameterName !== null && !in_array($parameter['name'], $parameterName)) {
                continue;
            }

            if (!($parameterId = $this->getParameterId($parameter, $field))) {
                continue;
            }

            if (!($parameterType = $this->getParameterType($parameterId))) {
                $parameterType = $parameter['type'];
            }

            $values = $item[$field];
            $explode = $parameter['explode'] ?? ',';

            if ($explode !== false) {
                $values = array_filter(array_map(function ($x) {
                    return trim($x);
                }, explode($explode, $item[$field])));
            }

            // apply modifier on values
            if ($parameter['modifier'] ?? false) {
                foreach ($values as &$tmp) {
                    $tmp = $parameter['modifier']($tmp);
                }
            }

            if ($parameterType === 'list') {
                $updateValues = [];
                foreach ((array) $values as $value) {
                    if (empty($value)) {
                        continue;
                    }

                    if ($valueId = $this->getParameterValue($parameterId, (string) $value)) {
                        $updateValues[] = $valueId;
                    }
                }
            } else {
                $updateValues = $values;
            }

            $updateValues = array_filter(array_map(function ($x) { return trim((string) $x); }, (array) $updateValues));

            $this->parameterFinder->setProductParameters($productId, $parameterId, $updateValues);
        }
    }

    protected function processTranslations(int $productId, array $item): void
    {
        $this->productsTranslation->saveSingleObject(
            'sk',
            $productId,
            [
                'title' => $item['slovensky-nazev'],
                'long_descr' => nl2br($item['popis-zbozi-sk']),
            ],
        );
    }

    protected function processProductPhotos(int $productId, string $code): void
    {
        if (isDevelopment()) {
            return;
        }

        // TMP: disable photos update in updateMode
        if ($this->updateMode) {
            return;
        }

        sqlGetConnection()->transactional(function () use ($code, $productId) {
            sqlQueryBuilder()
                ->delete('photos_products_relation')
                ->where(Operator::equals(['id_product' => $productId]))
                ->execute();

            $photos = $this->dataGoApi->getProductPhotos($code);
            foreach ($photos as $position => $photo) {
                if ($photoId = $this->photoDownloader->getPhoto($photo)) {
                    sqlQueryBuilder()
                        ->insert('photos_products_relation')
                        ->directValues([
                            'id_photo' => $photoId,
                            'id_product' => $productId,
                            'show_in_lead' => $position == 0 ? 'Y' : 'N',
                            'date_added' => (new \DateTime())->format('Y-m-d H:i:s'),
                            'position' => $position,
                        ])
                        ->execute();
                }
            }
        });
    }

    protected function getParameterType(int $parameterId): ?string
    {
        static $parameterTypeCache = [];

        if (!($parameterTypeCache[$parameterId] ?? false)) {
            $parameterType = sqlQueryBuilder()
                ->select('value_type')
                ->from('parameters')
                ->where(Operator::equals(['id' => $parameterId]))
                ->execute()->fetchColumn();

            if ($parameterType) {
                $parameterTypeCache[$parameterId] = $parameterType;
            }
        }

        return $parameterTypeCache[$parameterId] ?? null;
    }

    protected function getParameterFields(): array
    {
        $ageModifier = function ($value) {
            if (!is_numeric($value)) {
                return $value;
            }

            return round($value / 12);
        };

        return [
            'nazev-hlavni-skupiny-zbozi' => [
                'name' => 'Název hlavní skupiny zboži',
                'type' => 'list',
                'explode' => false,
            ],
            'nazev-podskupiny-zbozi' => [
                'name' => 'Název podskupiny zboži',
                'type' => 'list',
                'explode' => false,
            ],
            'charakteristika-1' => [
                'name' => 'Charakteristika 1',
                'type' => 'list',
                'explode' => false,
            ],
            'charakteristika-2' => [
                'name' => 'Charakteristika 2',
                'type' => 'list',
                'explode' => false,
            ],
            //            'charakteristika-3' => [
            //                'name' => 'Charakteristika 3',
            //                'type' => 'list',
            //            ],
            'produktova-rada' => [
                'name' => 'Produktová řada',
                'type' => 'list',
            ],
            'zeme-puvodu' => [
                'name' => 'Země původu',
                'type' => 'list',
            ],
            'celni-cislo' => [
                'name' => 'Celní číslo',
                'type' => 'char',
                'explode' => false,
            ],
            'celni-preference' => [
                'name' => 'Celní preference',
                'type' => 'char',
                'explode' => false,
            ],
            'baleni' => [
                'name' => 'Balení',
                'type' => 'float',
            ],
            'typ-baleni' => [
                'name' => 'Typ balení',
                'type' => 'list',
            ],
            'pocet-a-typ-baterii' => [
                'name' => 'Počet a typ baterií',
                'type' => 'list',
                'explode' => false,
            ],
            'baterie' => [
                'name' => 'Baterie',
                'type' => 'list',
                'explode' => false,
            ],
            'vek-od' => [
                'name' => 'Věk od',
                'type' => 'float',
                'modifier' => $ageModifier,
            ],
            'vek-do' => [
                'name' => 'Věk do',
                'type' => 'float',
                'modifier' => $ageModifier,
            ],
            'licence' => [
                'name' => 'Licence',
                'type' => 'list',
                'explode' => false,
            ],
            'recyklacni-prispevek-elektroodpad' => [
                'name' => 'Recyklační příspěvěk (elektroodpad)',
                'type' => 'float',
            ],
            'urceni-ciloveho-trhu' => [
                'name' => 'Určení cílového trhu',
                'type' => 'list',
            ],
            'typ-produktu' => [
                'name' => 'Typ produktu',
                'type' => 'list',
            ],
            'nazev-dodavatele' => [
                'name' => 'Název dodavatele',
                'type' => 'list',
                'explode' => false,
            ],
            'objednaci-cislo-u-dodavatele' => [
                'name' => 'Objednací číslo u dodavatele',
                'type' => 'char',
                'explode' => false,
            ],
            'assort' => [
                'name' => 'Assort',
                'type' => 'char',
            ],
            'jazykova-mutace' => [
                'name' => 'Jazyková mutace',
                'type' => 'list',
            ],
            'baterie-soucast-baleni' => [
                'name' => 'Baterie součást balení',
                'type' => 'list',
                'explode' => false,
            ],
            'status-typ' => [
                'name' => 'Status typ',
                'type' => 'list',
            ],
            'eans' => [
                'name' => 'EANs',
                'type' => 'list',
                'explode' => '|',
            ],
        ];
    }

    private function getItemVat(array $item): \Decimal
    {
        $vat = $item['dph-typ'];
        if (!is_numeric($vat)) {
            $vat = 21;
        }

        return toDecimal($vat);
    }

    protected function getCSVFileName(): string
    {
        if ($this->configuration->isNejhracka()) {
            return 'produkty-1673.csv';
        }

        return 'produkty-1363.csv';
    }

    private function getCSVFileNameByType(string $type): string
    {
        switch ($type) {
            case self::PROCESS_TYPE_NEW:
                if ($this->configuration->isNejhracka()) {
                    return 'produkty-1673-new.csv';
                }

                return 'produkty-1363-new.csv';
            case self::PROCESS_TYPE_DIFF:
                if ($this->configuration->isNejhracka()) {
                    return 'produkty-1673-diff.csv';
                }

                return 'produkty-1363-diff.csv';
        }

        return $this->getCSVFileName();
    }

    private function updatePriceForNewVat(int $productId, float $oldVat, float $newVat): void
    {
        $productData = sqlQueryBuilder()
            ->select('price, price_for_discount')
            ->from('products')
            ->where(Operator::equals(['id' => $productId]))
            ->sendToMaster()
            ->execute()->fetchAssociative();

        $productUpdate = [];

        // aktualizace vychozi ceny
        if ($productData['price']) {
            $productUpdate['price'] = toDecimal($productData['price'])->addVat($oldVat)->removeVat($newVat);
        }

        if ($productData['price_for_discount']) {
            $productUpdate['price_for_discount'] = toDecimal($productData['price_for_discount'])->addVat($oldVat)->removeVat($newVat);
        }

        if (!empty($productUpdate)) {
            sqlQueryBuilder()
                ->update('products')
                ->directValues($productUpdate)
                ->where(Operator::equals(['id' => $productId]))
                ->execute();
        }

        $priceListIds = [
            // Registrovaný uživatel - CZK
            2,
            // DMOC - CZK
            5,
        ];

        $qb = sqlQueryBuilder()
            ->select('id, price')
            ->from('pricelists_products')
            ->andWhere(Operator::inIntArray($priceListIds, 'id_pricelist'))
            ->andWhere(Operator::equals(['id_product' => $productId]));

        foreach ($qb->execute() as $item) {
            if (empty($item['price'])) {
                continue;
            }

            $priceWithoutVat = toDecimal($item['price'])->addVat($oldVat)->removeVat($newVat);

            sqlQueryBuilder()
                ->update('pricelists_products')
                ->directValues(['price' => $priceWithoutVat])
                ->where(Operator::equals(['id' => $item['id']]))
                ->execute();
        }
    }

    private function isProductVatChanged(int $productId, \Decimal $newVat): array
    {
        $vatId = sqlQueryBuilder()
            ->select('vat')
            ->from('products')
            ->where(Operator::equals(['id' => $productId]))
            ->sendToMaster()
            ->execute()->fetchOne();

        $vat = getVat($vatId);

        // DPH se zmenilo
        if ($newVat->asFloat() != $vat) {
            return [true, (float) $vat];
        }

        return [false, (float) $vat];
    }

    private function getParameterValue(int $parameterId, string $value): ?int
    {
        static $valuesCache = [];

        if (!($valuesCache[$parameterId] ?? false)) {
            $valuesCache[$parameterId] = Mapping::mapKeys(sqlQueryBuilder()
                ->select('id, COALESCE(JSON_VALUE(data, "$.datago_value"), value) as datago_value')
                ->from('parameters_list')
                ->where(Operator::equals(['id_parameter' => $parameterId]))
                ->execute()->fetchAll(),
                function ($k, $v) {
                    return [mb_strtolower($v['datago_value']), (int) $v['id']];
                });
        }

        // try to load from cache
        if ($valueId = ($valuesCache[$parameterId][mb_strtolower($value)] ?? false)) {
            return $valueId;
        }

        // parameter value does not exists, create new one
        if ($valueId = $this->parameterFinder->findParameterValue($parameterId, $value)) {
            // save to cache
            $valuesCache[$parameterId][mb_strtolower($value)] = (int) $valueId;
            // update custom data of value
            sqlQueryBuilder()
                ->update('parameters_list')
                ->set('data', 'JSON_SET(COALESCE(data, "{}"), "$.datago_value", :value)')
                ->where(Operator::equals(['id' => $valueId]))
                ->setParameter('value', $value)
                ->execute();
        }

        return null;
    }

    private function getParameterId(array $parameter, string $key): ?int
    {
        $parameterId = sqlQueryBuilder()
            ->select('id_parameter')
            ->from('datago_parameters')
            ->where(Operator::equals(['name' => $key]))
            ->execute()->fetchColumn();

        if (!$parameterId) {
            if (!($parameterId = $this->parameterFinder->findParameter($parameter['name'], $parameter['type']))) {
                return null;
            }

            sqlQueryBuilder()
                ->insert('datago_parameters')
                ->directValues(
                    [
                        'name' => $key,
                        'id_parameter' => $parameterId,
                    ]
                )->execute();
        }

        return (int) $parameterId;
    }

    private function getProducer(string $producer): ?int
    {
        static $producers = [];

        $key = mb_strtolower($producer);

        if (empty($producer)) {
            return null;
        }

        if (!$producers) {
            $producers = Mapping::mapKeys(
                sqlQueryBuilder()
                    ->select('id, name')
                    ->from('producers')
                    ->execute()->fetchAll(),
                function ($k, $v) {
                    return [mb_strtolower($v['name']), (int) $v['id']];
                }
            );
        }

        if (!($producers[$key] ?? false)) {
            $producers[$key] = sqlGetConnection()->transactional(function () use ($producer) {
                sqlQueryBuilder()
                    ->insert('producers')
                    ->directValues(['name' => $producer])
                    ->execute();

                return (int) sqlInsertId();
            });
        }

        return $producers[$key];
    }

    private function isItemValid(array $item): bool
    {
        // ignorovany status
        if (in_array($item['status-typ'], self::IGNORED_STATUSES)) {
            return false;
        }

        if (empty($item['nazev-zbozi'])) {
            return false;
        }

        return true;
    }

    private function getLastFileHashByType(string $type): ?string
    {
        $dbcfg = \Settings::getDefault();

        $dataGo = $dbcfg->loadValue('datago');

        return $dataGo[static::getType()][$type]['hash'] ?? null;
    }

    private function setLastFileHashByType(string $type, string $hash): void
    {
        $dbcfg = \Settings::getDefault();

        $dataGo = $dbcfg->loadValue('datago') ?? [];
        $dataGo[static::getType()][$type]['hash'] = $hash;
        $dataGo[static::getType()][$type]['updated'] = time();

        $dbcfg->saveValue('datago', $dataGo);
    }
}
