<?php

use KupShop\BonusProgramBundle\Utils\BonusComputer;
use KupShop\CatalogBundle\ProductList\FilterParams;
use KupShop\CatalogBundle\ProductList\MultiFetch;
use KupShop\CatalogBundle\ProductList\ProductCollection;
use KupShop\CatalogBundle\Query\RelatedProducts;
use KupShop\CatalogBundle\Util\ProductAvailabilityUtil;
use KupShop\I18nBundle\Translations\PhotosTranslation;
use KupShop\I18nBundle\Translations\ProductsUnitsTranslation;
use KupShop\I18nBundle\Util\PriceConverter;
use KupShop\KupShopBundle\Config;
use KupShop\KupShopBundle\Context\CurrencyContext;
use KupShop\KupShopBundle\Context\PosContext;
use KupShop\KupShopBundle\Context\PriceLevelContext;
use KupShop\KupShopBundle\Context\VatContext;
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\KupShopBundle\Util\Price\Price;
use KupShop\KupShopBundle\Util\Price\PriceLevelPrice;
use KupShop\KupShopBundle\Util\Price\PriceUtil;
use KupShop\KupShopBundle\Util\Price\ProductPrice;
use KupShop\KupShopBundle\Util\Suppliers\SuppliersUtil;
use KupShop\KupShopBundle\Wrapper\PriceWrapper;
use KupShop\OrderingBundle\Event\OrderItemEvent;
use KupShop\PricelistBundle\Context\PricelistContext;
use KupShop\ProductsChargesBundle\Util\ChargesUtil;
use KupShop\RestrictionsBundle\Utils\Restrictions;
use Query\Operator as Op;
use Query\Translation;

/**
 * Class representing one product.
 *
 * @author Joe
 */
#[AllowDynamicProperties]
class ProductBase implements ArrayAccess
{
    use DatabaseCommunication;

    /**
     * Product ID.
     *
     * @var int
     */
    public $id = -1;

    /**
     * Product texts.
     *
     * @var string
     */
    public $title = '';
    public $descr = '';
    public $longDescr = '';
    public $parameters = '';

    public $id_block;

    /**
     * SEO texts.
     *
     * @var string
     */
    public $meta_title = '';
    public $meta_description = '';
    public $meta_keywords = '';

    /**
     * Images.
     *
     * @var array
     */
    public $image = [];
    public $photos = [];
    public $photoId = -1;
    public $photoDescr = '';
    public $photoDateUpdate;

    /**
     * Prices.
     *
     * @var int
     */
    // cena nejlevnejsi varianty, nebo cena produktu, pokud nema varianty
    public $price = 0; // ve vysledku je to string, napr. "100 Kč"
    // priceRaw = p.price - cena produktu
    public $priceRaw = 0; // Decimal
    public $priceNoVat = 0; // string, napr. "83 Kč", skoro se nepouziva, muzeme smazat?
    public $priceCommon; // ProductPrice
    // cena nejdrazsi varianty, nebo cena produktu, pokud nema varianty
    public $priceMax = 0; // array
    public $pricelistPrice; // nepouziva se?

    public $pricelistId;

    /**
     * @var Decimal
     */
    public $discount;
    public $pricelistDiscount;
    public $vat = 0;
    public $vat_id = 0;

    /**
     * Nove promenne s cenou.
     *
     * @var Decimal[]
     */
    public $price_array = [];
    public $priceCommon_array = [];

    /**
     * @var ?array
     */
    public $producer;
    /**
     * Basic product informations.
     *
     * @var unknown_type
     */
    public $producerTitle;
    public $producerImage;
    public $idProducer;
    public $code = '';
    public $guarantee = 0;
    public $campaign = '';
    public $campaign_codes = [];
    public $charges;
    public $visible;

    /**
     * Stock and delivery time informations.
     *
     * @var int
     */
    public $inStore = 0;
    public $deliveryTime = 0;
    public $deliveryTimeText = '';
    public $deliveryTimeRaw = 0;
    protected $canBuy;
    protected $canWatch;

    /**
     * Additional informations.
     *
     * @var unknown_type
     */
    public $links = [];
    public $attachments = [];
    /**
     * @var ParameterConfiguration[]
     */
    public $configurations;
    public $param;
    public $related = [];
    public $commentsOpen = false;
    public $comments = [];
    public $variations;
    public $alsoBuyed = [];
    public $templates;

    public $sets;
    public $gifts;
    public $labels;
    public $multiSets;
    public $collections;
    public $products_related;
    public $matched_id_variation;

    public ?\KupShop\ComponentsBundle\Entity\Thumbnail $thumbnail;

    public $data;
    public $sections;

    public $width;
    public $height;
    public $depth;

    /** @var \KupShop\PricelistBundle\Util\Price\PriceListPrice */
    protected $productPrice;

    public $ean;
    public $weight;
    public $in_store_suppliers;
    public ?DateTimeImmutable $forecasted_delivery_date = null;
    public $note;
    public $priceOriginal; // PriceWrapper
    public $price_buy; // array|ProductPrice
    public $priceForDiscount; // ProductPrice
    public $reviews;
    public ?array $rating = null;
    public $variationId;
    public $bonus_points;
    public $unit;
    /**
     * @var string
     */
    public $note_;
    public $id_cn;

    public $productCode;
    public array $convertors = [];

    /** @var string{'Y', 'N'} */
    public string $showInSearch = 'Y';

    public ?int $position = null;
    public ?int $pieces_sold = null;

    /**
     * Constructor.
     */
    public function __construct($id = -1)
    {
        $this->id = $id;

        $this->discount = DecimalConstants::zero();
    }

    /**
     * Fetches data from database, product ID specified in $pid.
     *
     * @param int $pid
     *
     * @return bool
     */
    public function createFromDB($pid = null)
    {
        if ($pid) {
            $this->id = $pid;
        }

        $SQL = $this->createQueryBuilder()
            ->andWhere(Op::equals(['p.id' => $this->id]))
            ->execute();

        if (sqlNumRows($SQL) == 1) {
            $prod = sqlFetchAssoc($SQL);

            $this->title = $prod['title'];

            $this->createFromArray($prod);

            return true;
        } else {
            return false;
        }
    }

    /**
     * Fetches data from given array.
     *
     * @param array $data
     *
     * @return bool
     */
    public function createFromArray($data)
    {
        $this->id = $data['id'];
        $this->id_block = $data['id_block'] ?? null;
        $this->title = $data['title'];
        $this->discount = toDecimal($data['discount']);
        $this->vat = getVat($data['vat']);
        $this->vat_id = $data['vat'];
        $this->original_vat = $data['original_vat'] ?? $data['vat'];

        $this->deliveryTime = $data['delivery_time'];
        $this->code = $data['code'];

        $this->guarantee = $data['guarantee'] ?? null;
        $this->idProducer = $data['producer'] ?? null;
        $this->longDescr = trim($data['long_descr'] ?? '');
        $this->descr = trim($data['short_descr'] ?? '');
        $this->parameters = trim($data['parameters'] ?? '');
        $this->campaign = productCampaign($data['campaign'] ?? '', $this->campaign_codes);
        $this->inStore = $data['in_store'] ?? null;
        $this->visible = $data['figure'] ?? null;
        $this->ean = $data['ean'] ?? null;
        $this->weight = $data['weight'] ?? null;

        $this->meta_title = $data['meta_title'] ?? '';
        $this->meta_description = $data['meta_description'] ?? '';
        $this->meta_keywords = $data['meta_keywords'] ?? '';
        $this->matched_id_variation = getVal('matched_id_variation', $data, '');

        $this->data = getVal('data', $data);

        $this->width = $data['width'] ?? null;
        $this->height = $data['height'] ?? null;
        $this->depth = $data['depth'] ?? null;
        $this->showInSearch = $data['show_in_search'] ?? 'Y';
        $this->productCode = $data['productCode'] ?? '';
        $this->pieces_sold = $data['pieces_sold'] ?? null;
        $this->position = $data['position'] ?? null;

        if (findModule(Modules::PRICELISTS)) {
            if (array_key_exists('pricelist_discount', $data)) {
                $this->pricelistDiscount = toDecimal($data['pricelist_discount']);
                $pricelistContext = ServiceContainer::getService(\KupShop\PricelistBundle\Context\PricelistContext::class);
                $this->pricelistId = $pricelistContext->getActiveId();

                $extend = $pricelistContext->getActive()?->getUseProductDiscount();
                if (($extend && $this->pricelistId && !$this->pricelistDiscount->isZero()) || (!$extend && $this->pricelistId)) {
                    $this->discount = $this->pricelistDiscount;
                }
            }
        }

        if (array_key_exists('id_photo', $data)) {
            $this->photoId = $data['id_photo'];
            $this->photoDescr = $data['descr_photo'];
            $this->photoDateUpdate = $data['id_photo_update'];
        }

        if (!empty($data['in_store_suppliers'])) {
            $this->in_store_suppliers = $data['in_store_suppliers'];
        }

        if (findModule('products', 'note')) {
            $this->note_ = $data['note_'] ?? '';
        }

        if (isset($data['price'])) {
            $this->fetchPrices($data);
        }

        if (findModule(Modules::BONUS_PROGRAM) && isset($data['price'])) {
            $this->bonus_points = (is_null($data['bonus_points']) ? null : toDecimal($data['bonus_points']));
        }

        if (findModule(Modules::PRODUCTS, Modules::SUB_UNITS)) {
            $this->fetchUnit($data);
        }

        $this->id_cn = getVal('id_cn', $data);
        $this->date_added = $data['date_added'] ?? '';

        if (isset($data['in_store_min'])) {
            $this->in_store_min = $data['in_store_min'];
        }

        if (isset($data['date_stock_in']) && $data['date_stock_in'] !== '0000-00-00 00:00:00') {
            try {
                $this->date_stock_in = new DateTime($data['date_stock_in']);
            } catch (Exception $e) {
            }
        }

        if (!empty($data['variationsIds'])) {
            $this->variationsIds = array_filter(explode(',', $data['variationsIds']));
        }

        if (array_key_exists('has_variations', $data)) {
            $this->has_variations = (bool) $data['has_variations'];
        }

        if (findModule(Modules::PRODUCTS_SERIAL_NUMBERS)) {
            $this->serial_number_require = $data['serial_number_require'] ?? 'N';
        }

        return true;
    }

    public function fetchProducer()
    {
        if ($this->producer) {
            return $this->producer;
        }

        if (findModule(Modules::PRODUCERS)) {
            $multiFetch = ServiceContainer::getService(MultiFetch::class);
            $multiFetch->fetchProducers(new ProductCollection([$this->id => $this]));
        }

        return $this->producer;
    }

    public function fetchPrices($data)
    {
        $currencyContext = Contexts::get(CurrencyContext::class);
        $priceConverter = ServiceContainer::getService(PriceConverter::class);

        $this->productPrice = new ProductPrice(toDecimal($data['price']), $currencyContext->getDefault(), getVat($data['vat']), $data['discount']);
        $this->productPrice->setSource($this);

        $priceForDiscountCurrency = $currencyContext->getDefault();

        if (findModule(Modules::PRICELISTS) && isset($data['pricelist_price'])) {
            /** @var \KupShop\PricelistBundle\Entity\Pricelist $activePricelist */
            $activePricelist = Contexts::get(PricelistContext::class)->getActive();

            $extend = $activePricelist?->getUseProductDiscount();
            if (($extend && $this->pricelistId && isset($data['pricelist_discount'])) || (!$extend && $this->pricelistId)) {
                $data['discount'] = $data['pricelist_discount'];
            }

            if (!$data['pricelist_currency']) {
                $data['pricelist_currency'] = $currencyContext->getDefault();
            } else {
                $data['pricelist_currency'] = $currencyContext->getALL()[$data['pricelist_currency']];
            }

            // správně by se mělo brát z ceníků - nyní se v případě různosti měn porovnavali například EUR s CZK
            $data['priceMax'] = $data['pricelist_price_max'] ?? $data['pricelist_price'];

            $originalPrice = $this->productPrice;
            $this->productPrice = new \KupShop\PricelistBundle\Util\Price\PriceListPrice(toDecimal($data['pricelist_price']), $data['pricelist_currency'], getVat($data['vat']), $data['discount']);
            $this->productPrice->setOriginalPrice($originalPrice);
            $this->productPrice->setSource($this);

            $coefficient = $activePricelist?->getCoefficient();
            if ($coefficient && in_array($data['price_source'], ['p', 'pv'])) {
                /* v pripade koeficientu na ceniku je aplikovan i na originalPrice */
                $this->productPrice->applyCoefficient(toDecimal($coefficient));
                if (!empty($data['price_for_discount'])) {
                    $data['price_for_discount'] *= $coefficient;
                }
                if (!empty($data['priceMax'])) {
                    $data['priceMax'] *= $coefficient;
                }
            }

            $this->priceOriginal = PriceWrapper::wrap($this->productPrice->getOriginalPrice());
            $this->priceRaw = $priceConverter->convert($this->productPrice->getCurrency(), $currencyContext->getDefault(), $this->productPrice->getValue());

            // prerazim CPS od produktu CPS z ceniku, pokud ji mam dostupnou
            if (!empty($data['pricelist_price_history']) && array_key_exists('pricelist_price_for_discount', $data)) {
                $data['price_for_discount'] = $data['pricelist_price_for_discount'];
                $priceForDiscountCurrency = $data['pricelist_currency'];
            }
        } else {
            $priceOriginal = new ProductPrice(toDecimal($data['priceRaw']), $currencyContext->getDefault(), getVat($data['vat']));
            $priceOriginal->setSource($this);
            $this->priceOriginal = PriceWrapper::wrap($priceOriginal);
            $this->priceRaw = $priceConverter->convert($this->productPrice->getCurrency(), $currencyContext->getDefault(), $data['priceRaw']);
        }

        $originalProductPrice = $this->productPrice;

        $pricelevelContext = Contexts::get(PriceLevelContext::class);
        if ($pricelevel = $pricelevelContext->getActive()) {
            $this->productPrice = new PriceLevelPrice($this->productPrice);
            $IDcat = isset($data['priceLevelSections']) ? explode(',', $data['priceLevelSections']) : null;
            $pricelevelDiscount = $pricelevel->getDiscount($this->id, $IDcat, $this->idProducer, $this);
            $this->productPrice->setPricelevelDiscount($pricelevelDiscount);
            $this->productPrice->setSource($this);
        }

        if (!findModule(\Modules::PRODUCTS, \Modules::SUB_MODERN_PRICES)) {
            // Legacy prices - preformatted, old "array" prices, ...

            $IDcategory = $this['priceLevelSections'] ?? null;
            $IDproducer = $this->idProducer ?? -1;

            $this->price_array = formatCustomerPrice($priceConverter->convert($originalProductPrice->getCurrency(), $currencyContext->getDefault(), $originalProductPrice->getValue()), $originalProductPrice->getDiscount(), $originalProductPrice->getVat()->asFloat(), $this->id, $IDcategory, $IDproducer, null, $currencyContext->getDefault());

            if (isset($data['priceMax'])) {
                $this->priceMax = formatCustomerPrice($data['priceMax'], $originalProductPrice->getDiscount(), $originalProductPrice->getVat()->asFloat(), $this->id, $IDcategory, $IDproducer);
            }

            $this->priceCommon_array = formatPrice(applyCurrency($data['price_common']), 0);

            $this->price = printPrice($this->price_array, ['printdealerdiscount' => true, 'currency' => $currencyContext->getActive()->getId()]);

            $this->priceNoVat = printPrice($this->price_array, ['withVat' => false]);

            if (array_key_exists('price_buy', $data)) {
                $this->price_buy = formatPrice(applyCurrency($data['price_buy']), $originalProductPrice->getVat()->asFloat());
            }
        } else {
            // TODO: Zabránit vzniku, šetřit čas
            unset($this->priceOriginal);

            if (isset($data['priceMax'])) {
                $this->priceMax = new ProductPrice(toDecimal($data['priceMax']), $originalProductPrice->getCurrency(), $originalProductPrice->getVat(), $originalProductPrice->getDiscount());
                if ($this->productPrice instanceof PriceLevelPrice) {
                    $this->priceMax = new PriceLevelPrice($this->priceMax);
                    $this->priceMax->setPricelevelDiscount($this->productPrice->getPricelevelDiscount());
                }
                $this->priceMax->setSource($this);
            }

            if (array_key_exists('price_buy', $data)) {
                $this->price_buy = new ProductPrice(toDecimal($data['price_buy']), $currencyContext->getDefault(), getVat($data['vat']));
                $this->price_buy->setSource($this);
            }

            // TODO: Jediný co zbyde "divný" je priceRaw. Zkusit najít použití a případně zabít. Pak už zbyde jen productPrice, jupí! :-)
        }

        if ($data['price_common'] > 0) {
            $price_common = toDecimal($data['price_common'])->removeVat($this->productPrice->getVat());
            $this->priceCommon = new ProductPrice($price_common, $currencyContext->getDefault(), $this->productPrice->getVat());
            $this->priceCommon->setSource($this);
        }

        if (array_key_exists('price_for_discount', $data)) {
            $price_for_discount = toDecimal($data['price_for_discount']);
            $this->priceForDiscount = new ProductPrice($price_for_discount, $priceForDiscountCurrency, $this->productPrice->getVat());
            $this->priceForDiscount->setSource($this);
        }

        $this->recalculatePricesVatFromPriceWithVat($this);
    }

    protected function recalculatePricesVatFromPriceWithVat(Product $product): void
    {
        if (!PriceUtil::isProductPricesVatFromTop()) {
            return;
        }

        $fields = ['priceOriginal', 'productPrice', 'priceForDiscount'];

        $originalVatId = $product->original_vat ?? $product->vat_id;

        $originalVat = (float) getVat($originalVatId);
        $vatContext = Contexts::get(VatContext::class);
        if ($vatContext->getActive() == $vatContext::NO_VAT) {
            $originalVat = $vatContext->getVat($originalVatId)['vat'];
        }

        foreach ($fields as $field) {
            if (!isset($product->{$field})) {
                continue;
            }

            // PriceLevelPrice se uvnitr pocita specialne :/
            if ($product->{$field} instanceof PriceLevelPrice) {
                $product->{$field}->setOriginalPrice(
                    PriceUtil::recalculatePriceVatFromPriceWithVat(
                        $product->{$field}->getOriginalPrice(),
                        $originalVat,
                        $product->vat_id
                    )
                );
            }

            // prepocitam cenu tak, aby DPH bylo pocitano ze shora
            $product->{$field} = PriceUtil::recalculatePriceVatFromPriceWithVat($product->{$field}, $originalVat, $product->vat_id);
        }

        $currencyContext = Contexts::get(CurrencyContext::class);
        $priceConverter = ServiceContainer::getService(PriceConverter::class);

        // do price_array dam product price, aby to tam bylo taky prepocitany
        $this->price_array = PriceWrapper::wrap($product->getProductPrice());
        // prepocitam price raw podle aktualni productPrice - kvuli kosiku
        $priceRaw = $this->getProductPrice()->getValue();
        // pokud se jedna o cenu pocitanou pres price level, tak si do price raw vytahnu cenu z puvodni ceny
        // protoze v $value na PriceLevelPrice je ulozena cena, na kterou je aplikovana uz ta cenova hladina
        // a takovou cenu nechceme, protoze to neni priceRaw
        if ($this->getProductPrice() instanceof PriceLevelPrice) {
            $priceRaw = $this->getProductPrice()->getOriginalPrice()->getValue();
        }

        $this->priceRaw = $priceConverter->convert($this->getProductPrice()->getCurrency(), $currencyContext->getDefault(), $priceRaw);
    }

    public function fetchCharges($data = false, bool $onlyVisible = true)
    {
        if (!findModule(Modules::PRODUCTS_CHARGES)) {
            return [];
        }

        $util = ServiceContainer::getService(ChargesUtil::class);

        if ($this->charges === null) {
            $multiFetch = ServiceContainer::getService(MultiFetch::class);
            $multiFetch->fetchProductsCharges(new ProductCollection([$this->id => $this]), $onlyVisible);

            $this->charges = $util->applyPrices($this->charges, $this);
        }

        return $util->applyData($this->charges, $data, $this);
    }

    /**
     * @param $products   Product[] array of products
     * @param $parameters int[] array of parameter IDs
     *
     * @deprecated Use KupShop\CatalogBundle\ProductList\MultiFetch service instead
     *
     * @return array
     */
    public static function fetchParametersMulti($products, $parameters = null)
    {
        $multiFetch = ServiceContainer::getService(\KupShop\CatalogBundle\ProductList\MultiFetch::class);

        if (!$products instanceof \KupShop\CatalogBundle\ProductList\ProductCollection) {
            // Add idd to keys
            $products = array_combine(array_column($products, 'id'), $products);
            $products = new \KupShop\CatalogBundle\ProductList\ProductCollection($products);
        }

        return $multiFetch->fetchParameters($products, $parameters);
    }

    public function fetchParameters($parameters = null)
    {
        if (is_array($this->param)) {
            return $this->param;
        }

        if (findModule('products_parameters')) {
            self::fetchParametersMulti([$this], $parameters);
        } else {
            $this->param = [];
        }

        return $this->param;
    }

    /**
     * @param $products Product[]|ProductCollection
     */
    public static function fetchSetsMulti($products, $showOutOfStock = null): void
    {
        if (isset($products[0]->sets)) {
            return;
        }

        if (is_array($products)) {
            $products = new ProductCollection($products); // legacy support for $products as array
        }

        $multiFetch = ServiceContainer::getService(\KupShop\CatalogBundle\ProductList\MultiFetch::class);
        $multiFetch->fetchSets($products, $showOutOfStock);
    }

    /**
     * @return array
     */
    public function fetchGifts($showOutOfStock = null)
    {
        if (is_array($this->gifts)) {
            return $this->gifts;
        }

        $this->gifts = [];
        if (\findModule(\Modules::PRODUCT_GIFTS)) {
            $multiFetch = ServiceContainer::getService(\KupShop\CatalogBundle\ProductList\MultiFetch::class);
            $multiFetch->fetchGifts(new \KupShop\CatalogBundle\ProductList\ProductCollection([$this->id => $this]), $showOutOfStock);
        }

        return $this->gifts;
    }

    public function fetchLabels($visibility = null)
    {
        if ($this->labels) {
            return $this->labels;
        }

        if (\findModule(\Modules::LABELS, Modules::SUB_PRODUCT_LABELS)) {
            $multiFetch = ServiceContainer::getService(MultiFetch::class);
            $multiFetch->fetchProductLabels(new ProductCollection([$this->id => $this]), $visibility);
        } else {
            $this->labels = [];
        }

        return $this->labels;
    }

    /**
     * @return array(Product)
     */
    public function fetchSets($showOutOfStock = null)
    {
        if (is_array($this->sets)) {
            return $this->sets;
        }

        if (findModule(\Modules::PRODUCT_SETS)) {
            self::fetchSetsMulti([$this->id => $this], $showOutOfStock);
        } else {
            $this->sets = [];
        }

        return $this->sets;
    }

    /**
     * @return \KupShop\CatalogBundle\Entity\Section[]
     */
    public function fetchSections()
    {
        if (is_array($this->sections)) {
            return $this->sections;
        }

        $multiFetch = ServiceContainer::getService(\KupShop\CatalogBundle\ProductList\MultiFetch::class);

        $multiFetch->fetchSections(new \KupShop\CatalogBundle\ProductList\ProductCollection([$this->id => $this]));

        return $this->sections;
    }

    /**
     * @param bool $fetchAll
     *
     * @return array(Product)
     */
    public function fetchCollections($fetchAll = true)
    {
        if (is_array($this->collections)) {
            return $this->collections;
        }

        $dbcfg = Settings::getDefault();

        $this->collections = [];

        if (findModule('products_collections')) {
            $qb = sqlQueryBuilder()
                ->select('p.id, p.figure, p.title, p.code, MIN(COALESCE(pv.price, p.price)) as price, MAX(COALESCE(pv.price, p.price)) as priceMax,
				p.price_common, p.vat, p.discount, p.producer, p.delivery_time, p.campaign, p.figure, p.in_store, p.price as priceRaw');

            if (findModule(\Modules::BONUS_PROGRAM)) {
                $qb->addSelect('COALESCE(pv.bonus_points, p.bonus_points) as bonus_points');
            }

            if (findModule(Modules::PRODUCTS, Modules::SUB_UNITS)) {
                $qb->addSelect('p.unit, pu.short_name, pu.long_name as unit_long_name')
                    ->leftJoin('p', 'products_units', 'pu', 'p.unit=pu.id');
                if (findModule(Modules::PRODUCTS, Modules::SUB_UNITS_FLOAT)) {
                    $qb->addSelect('pu.pieces_precision');
                }
            }

            if (findModule(Modules::RESTRICTIONS)) {
                $restrictions = ServiceContainer::getService(Restrictions::class);
                $qb->andWhere($restrictions->getRestrictionSpec());
            }

            $qbOthers = clone $qb;

            $qb->addSelect('ps.id_product = :id_product as own')
                ->from('products_collections', 'ps')
                ->leftJoin('ps', 'products', 'p', 'p.id = IF(ps.id_product = :id_product, ps.id_product_related, ps.id_product)')
                ->leftJoin('p', 'products_variations', 'pv', 'p.id=pv.id_product')
                ->andWhere('(ps.id_product=:id_product OR ps.id_product_related=:id_product)')
                ->groupBy('p.id')
                ->setParameter('id_product', $this->id);

            $own = [];
            $others = [];
            foreach ($qb->execute() as $row) {
                $product = new Product();
                $product->createFromArray($row);

                if ($row['own']) {
                    if ($dbcfg->prod_show_not_in_store == 'N' && !isAdministration() && $row['in_store'] <= 0) {
                        continue;
                    } else {
                        $own[$product->id] = $product;
                    }
                } else {
                    $others[$product->id] = $product;
                }
            }

            if ($others && $fetchAll) {
                $qbOthers->addSelect('ps.id_product');

                if ($dbcfg->prod_show_not_in_store == 'N' && !isAdministration()) {
                    $qbOthers->andWhere('p.in_store > 0');
                }

                $qbOthers->from('products_collections', 'ps')
                    ->leftJoin('ps', 'products', 'p', 'p.id = ps.id_product_related')
                    ->leftJoin('p', 'products_variations', 'pv', 'p.id=pv.id_product')
                    ->andWhere(Op::andX(Op::inIntArray(array_keys($others), 'ps.id_product'),
                        Op::not(Op::equals(['p.id' => $this->id]))))
                    ->groupBy('p.id')
                    ->orderBy('ps.id_product');

                foreach ($others as &$other_prod) {
                    if ($dbcfg->prod_show_not_in_store == 'N' && !isAdministration() && $other_prod->inStore <= 0) {
                        $other_prod->visible = 'N';
                    }
                }

                foreach ($qbOthers->execute() as $other) {
                    $product = new Product();
                    $product->createFromArray($other);

                    $others[$other['id_product']]->collections[] = $product;
                }
            }

            $this->collections = ['own' => $own, 'others' => $others];
        }

        return $this->collections;
    }

    public function fetchProductsRelated()
    {
        if (is_array($this->products_related)) {
            return $this->products_related;
        }

        $this->products_related = [];

        if (findModule('products_related')) {
            $SQL = sqlQueryBuilder()
                ->select('p.title')->fromProducts()
                ->addSelect(RelatedProducts::relatedProductsSpec([$this->id]));

            if (findModule(Modules::DYNAMIC_RELATED_PRODUCTS)) {
                $SQL->andWhere(Op::isNull('pr.id_products_related_dynamic'));
            }

            foreach ($SQL->execute() as $row) {
                $this->products_related[] = $row;
            }
        }

        return $this->products_related;
    }

    public function fetchRating(): ?array
    {
        if ($this->rating !== null) {
            return $this->rating;
        }

        $this->rating = [];

        if (findModule(Modules::REVIEWS)) {
            $multiFetch = ServiceContainer::getService(MultiFetch::class);
            $multiFetch->fetchRating(new ProductCollection([$this->id => $this]));
        }

        return $this->rating;
    }

    // backward compatibility
    public function fetchReviews()
    {
        return $this->reviews ??= $this->fetchRating();
    }

    public function isVisible()
    {
        return $this->visible != 'N';
    }

    public function isOld()
    {
        return $this->visible == 'O';
    }

    public function canBuy(): bool
    {
        if (isset($this->canBuy)) {
            return $this->canBuy;
        }

        if (empty($this->deliveryTimeText)) {
            $this->prepareDeliveryText();
        }

        return ProductAvailabilityUtil::canBuy($this->deliveryTime, $this->isOld(), $this->canWatch, $this->canBuy);
    }

    public function canWatch(): ?bool
    {
        if (!findModule(Modules::WATCHDOG)) {
            return null;
        }

        if (!isset($this->canWatch)) {
            $this->canBuy();
        }

        return $this->canWatch ?? false;
    }

    public function hasVariations()
    {
        return $this->has_variations ??= returnSQLResult('SELECT COUNT(*) FROM '.getTableName('products_variations')." pv WHERE pv.id_product={$this->id}") > 0;
    }

    public function findVariation($id)
    {
        if (!empty($this->variations['variations'][$id])) {
            return $this->variations['variations'][$id];
        }

        return null;
    }

    public function fetchVariations($in_store_only = false)
    {
        if (!is_null($this->variations)) {
            return $this->variations;
        }

        $this->variations = [];

        if (findModule('products_variations')) {
            // nacist varianty k produktu
            $this->variations['variations'] = Variations::getProductVariations(intval($this->id), $in_store_only);

            // docist jmenovky, pokud jsou varianty
            if (count($this->variations['variations']) > 0) {
                $this->variations['labels'] = Variations::getProductLabels(intval($this->id));
            }
        }

        return $this->variations;
    }

    /**
     * @param $products Product[]
     */
    public static function fetchVariationsMulti($products, $labels)
    {
        $multiFetch = ServiceContainer::getService(\KupShop\CatalogBundle\ProductList\MultiFetch::class);

        if (!$products instanceof \KupShop\CatalogBundle\ProductList\ProductCollection) {
            $products = new \KupShop\CatalogBundle\ProductList\ProductCollection($products);
        }

        $multiFetch->fetchVariations($products, $labels);
    }

    public function fetchLinks()
    {
        $SQL = sqlQuery('SELECT *
				FROM '.getTableName('links')."
				WHERE id_product='".$this->id."' ");

        foreach ($SQL as $key => $link) {
            if (empty($link['title'])) {
                $link['title'] = '['.($key + 1).']';
            }

            $this->links[] = $link;
        }

        sqlFreeResult($SQL);
    }

    public function fetchAttachments()
    {
        $SQL = sqlQueryBuilder()->select('*')
            ->from('attachments')
            ->where(Op::equals(['id_product' => $this->id]))
            ->orderBy('position', 'ASC')
            ->execute();

        foreach ($SQL as $attachment) {
            $link = $attachment['link'];
            $attachment['remote'] = preg_match('@^([a-z]{2,4}://)@i', $link);

            if (!$attachment['remote'] && isset($link[0]) && $link[0] == '/') {
                $attachment['size'] = @filesize('.'.urldecode($link));
            }

            $this->attachments[] = $attachment;
        }

        sqlFreeResult($SQL);
    }

    public function fetchImages($mainSize, $additionalSize = null)
    {
        if ($mainSize) {
            if ($this->photoId != -1) {
                $this->image = getImage($this->photoId, null, null, $mainSize, $this->photoDescr, strtotime($this->photoDateUpdate ?? ''));
            } else {
                $this->image = leadImage($this->id, $mainSize);
            }
        }

        if ($additionalSize) {
            $qb = sqlQueryBuilder()
                ->select('ph.id, ph.descr, ph.source, ph.image_2, ppr.show_in_lead, ph.date_update')
                ->from('photos_products_relation', 'ppr')
                ->leftJoin('ppr', 'photos', 'ph', 'ppr.id_photo = ph.id')
                ->where(Translation::coalesceTranslatedFields(PhotosTranslation::class))
                ->andWhere('ppr.id_product=:id_product AND ppr.active="Y"')
                ->groupBy('ph.id')
                ->setParameter('id_product', $this->id)
                ->orderBy('position', 'ASC');

            if (findModule(Modules::PRODUCTS_VARIATIONS_PHOTOS)) {
                $qb->having('MAX(ppr.show_in_lead) != \'Y\'');
            } else {
                $qb->andWhere('ppr.show_in_lead != "Y"');
            }

            if (findModule(Modules::VIDEOS)) {
                $qb->addSelect('id_cdn id_video')
                    ->leftJoin('ph', 'videos', 'v', 'v.id_photo = ph.id');
            }

            $SQL = $qb->execute();

            while (($image = sqlFetchAssoc($SQL)) !== false) {
                // skip main image, that is attached to an variation
                if ($image['id'] == ($this->image['id'] ?? null)) {
                    continue;
                }

                if (empty($image['image_2'])) {
                    continue;
                }

                $this->photos[] = array_merge(
                    getImage($image['id'], $image['image_2'], $image['source'], $additionalSize, $image['descr'], strtotime($image['date_update'])),
                    ['show_in_lead' => $image['show_in_lead'], 'id_video' => $image['id_video'] ?? null]
                );
            }

            sqlFreeResult($SQL);
        }
    }

    /**
     * Sanitize and fill-in texts.
     */
    public function prepareTexts()
    {
        // Sanitize text
        $this->title = htmlspecialchars($this->title);
    }

    public function prepareDeliveryText()
    {
        $cfg = Config::get();

        $inStore = $this->inStore;

        $this->deliveryTimeRaw = $this->deliveryTime;
        $this->deliveryTimeText = getProductDeliveryText($inStore, $this->deliveryTime);

        if ($inStore <= 0) {
            $inStoreSuppliers = $this->getInStoreSuppliers();
            if (findModule(\Modules::PRODUCTS_SUPPLIERS, \Modules::SUB_ALLOW_NEGATIVE_IN_STORE)) {
                $inStoreSuppliers += $this->inStore;
            }

            if ($inStoreSuppliers > 0) {
                if (findModule(\Modules::PRODUCTS_SUPPLIERS) && findModule(\Modules::PRODUCTS_SUPPLIERS, \Modules::SUB_DELIVERY_TIME)) {
                    $this->deliveryTime = $cfg['Modules']['products_suppliers']['delivery_time'];
                    $languageContext = \KupShop\KupShopBundle\Util\Contexts::get(\KupShop\KupShopBundle\Context\LanguageContext::class);

                    if (!$languageContext->translationActive()) {
                        $this->deliveryTimeText = sprintf($cfg['Products']['DeliveryTime'][$cfg['Modules']['products_suppliers']['delivery_time']], strval($inStoreSuppliers));
                    } else {
                        $this->deliveryTimeText = sprintf(translate_shop($this->deliveryTime, 'deliveryTime'), strval($inStoreSuppliers));
                    }
                }
            }
            $this->inStore = $inStore;
            $this->in_store_suppliers = $inStoreSuppliers;
        }
    }

    /**
     * @param Decimal $pieces
     *
     * @return Decimal
     */
    public function getPrice($variationId, $data, $pieces)
    {
        $price = toDecimal($this->priceRaw);

        if (!is_null($variationId)) {
            $price = Variations::getCustomPrice($variationId, $price);
        }

        $dispatcher = ServiceContainer::getService('event_dispatcher');
        $event = $dispatcher->dispatch(new OrderItemEvent($this, $variationId, $price, $pieces, $data), OrderItemEvent::CALCULATE_PRICE);
        $price = $event->getPrice();

        if (findModule(\Modules::PRODUCTS_PARAMETERS, \Modules::SUB_CONFIGURATIONS) && !empty($data['configurations'])) {
            $this->fetchParameters();

            $config = $data['configurations'];
            foreach ($this->configurations as $id_parameter => $parameter) {
                $values = $parameter->fetchValues(false);
                if (!empty($config[$id_parameter]) && array_key_exists($config[$id_parameter], $values)) {
                    $configurationPriceWithVat = $values[$config[$id_parameter]]->configuration_price;
                    $price = $price->add($configurationPriceWithVat->removeVat($this->vat)->removeDiscount($this->discount));
                }
            }
        }

        return $price;
    }

    public function printNote($data)
    {
        $ret = '';

        if (findModule(\Modules::PRODUCTS_PARAMETERS, \Modules::SUB_CONFIGURATIONS) && !empty($data['configurations'])) {
            $this->fetchParameters();

            $parts = [];
            $config = $data['configurations'];
            foreach ($this->configurations as $id_parameter => $parameter) {
                $values = $parameter->fetchValues();
                if (!empty($config[$id_parameter]) && array_key_exists($config[$id_parameter], $values)) {
                    $value = $values[$config[$id_parameter]];
                    $parts[] = "{$parameter->name}: {$value->value}";
                } elseif ($parameter['value_type'] == 'char' && !empty($config[$id_parameter])) {
                    $parts[] = "{$parameter->name}: {$config[$id_parameter]}";
                }
            }

            if ($parts) {
                $ret .= join(', ', $parts);
            }
        }

        return $ret;
    }

    public function parseNote($data): array
    {
        if (!$data) {
            return [];
        }

        $data = json_decode($data, true);

        return is_array($data) ? $data : [];
    }

    public function dumpNote($data)
    {
        return json_encode($data);
    }

    public function storeIn($variationId, $count)
    {
        $posContext = Contexts::get(PosContext::class);
        if (!$posContext->getActiveId() && findModule('orders', \Modules::SUB_STORE_IN_DISABLE) && $count >= 0) {
            return;
        }

        if (!empty($variationId)) {
            $table = getTableName('products_variations');
            $id = $variationId;
        } else {
            $table = getTableName('products');
            $id = $this->id;
        }

        $query = "SELECT in_store FROM {$table}
	   WHERE id={$id} FOR UPDATE";
        if (($inStoreOrigin = returnSQLResult($query)) !== false) {
            $inStoreNew = $inStoreOrigin + $count;

            if ($count < 0 && $inStoreNew < 0) {
                $reserved = max($inStoreOrigin, 0);
            } else {
                $reserved = -$count;
            }

            sqlQuery("UPDATE {$table}
	            SET in_store={$inStoreNew}
	            WHERE id={$id}");
        } else {
            return logError(__FILE__, __LINE__, "Variation id {$variationId} does not exists for product id {$this->id}!");
        }

        // React to store depletion
        if ($inStoreNew == 0 && $inStoreOrigin > 0) {
            $dbcfg = Settings::getDefault();

            if (!empty($variationId)) {
                if ($dbcfg->prod_do_after_order == 'delete') {
                    sqlQuery('DELETE FROM '.getTableName('products_variations')."
	                  WHERE id={$variationId}");
                }
                if ($dbcfg->prod_do_after_order == 'status') {
                    sqlQuery('UPDATE '.getTableName('products_variations')."
	                  SET delivery_time='-2'
	                  WHERE id={$variationId}");
                }
                if ($dbcfg->prod_do_after_order == 'hide') {
                    $this->storeInHideAction($variationId);
                }
            } else {
                if ($dbcfg->prod_do_after_order == 'delete') {
                    $this->delete();
                }
                if ($dbcfg->prod_do_after_order == 'status') {
                    sqlQuery('UPDATE '.getTableName('products')."
	                  SET delivery_time='-2'
	                  WHERE id={$this->id}");
                }
                if ($dbcfg->prod_do_after_order == 'hide') {
                    sqlQuery('UPDATE '.getTableName('products')."
               SET figure='O'
               WHERE id={$this->id}");
                }
            }
        }

        $this->updateInStore();

        return $reserved;
    }

    public function storeInHideAction($variationId)
    {
        sqlQuery('UPDATE '.getTableName('products_variations')."
      	            SET figure='N'
      	            WHERE id={$variationId}");

        $this->updateInStore();

        // hide also whole project if whole product is not available
        if ($this->inStore <= 0) {
            sqlQuery('UPDATE '.getTableName('products')."
   	               SET figure='O'
   	               WHERE id={$this->id}");
        }
    }

    public function sell($variationId, $count)
    {
        $reserved = null;

        sqlGetConnection()->transactional(function () use ($variationId, $count, &$reserved) {
            // Update pieces_sold in all cases
            sqlQuery('UPDATE products
	         SET pieces_sold=pieces_sold+:count
	         WHERE id=:id',
                ['id' => $this->id, 'count' => $count]
            );

            // Check if store management is enabled
            if (Settings::getDefault()->prod_subtract_from_store != 'Y') {
                return;
            }

            // Subtract items from store
            $reserved = $this->storeIn($variationId, -$count);
        });

        return $reserved;
    }

    public function updateInStore()
    {
        sqlQuery("UPDATE products p
	         SET p.in_store=(
            SELECT COALESCE(SUM(GREATEST(pv.in_store, 0)), p.in_store) 
            FROM products_variations pv
            WHERE pv.id_product=p.id
            )
            WHERE p.id={$this->id}");

        $this->inStore = returnSQLResult("SELECT p.in_store
	      FROM products p 
	      WHERE p.id={$this->id}");
    }

    public function updateDeliveryTime()
    {
        global $cfg;

        $delivery_times = sqlQueryBuilder()->select('delivery_time')->from('products_variations')
            ->where(
                Op::equals([
                    'id_product' => $this->id,
                    'figure' => 'Y',
                ]))
            ->execute()->fetchAll();

        if (empty($delivery_times)) {
            return true;
        }

        $minDelay = null;
        $bestID = null;
        foreach ($delivery_times as $variation) {
            $index = $variation['delivery_time'];
            switch ($index) {
                case -2:
                    $delay = 99;
                    break;
                case -1:
                    $delay = 2.5;
                    break;
                default:
                    $delay = abs($index);
                    break;
            }
            $delay = $cfg['Products']['DeliveryTimeDays'][$variation['delivery_time']] ?? $delay;
            if ($minDelay === null || $delay < $minDelay) {
                $minDelay = $delay;
                $bestID = $index;
            }
        }

        $this->deliveryTime = $bestID;

        return sqlAffectedRows(sqlQuery('UPDATE '.getTableName('products')." p
   	   SET p.delivery_time='{$this->deliveryTime}'
   	   WHERE p.id={$this->id}"));
    }

    // Delete product or variation based on $variation_id. If $variation_id is null, delete product. If it is last variation, delete also product.
    public function deleteVariation($variation_id = null)
    {
        $ret = 0;
        $variationsCount = returnSqlResult("SELECT COUNT(*)
	         FROM products_variations pv
	         WHERE pv.id_product={$this->id}");

        if ($variationsCount > 0 && $variation_id == null) {
            return $ret;
        }

        if ($variation_id) {
            sqlQuery("UPDATE products_variations SET figure='N' WHERE id={$variation_id}");
            $ret++;

            $emptyProduct = returnSqlResult("SELECT COUNT(*) > 0
	         FROM products_variations pv 
	         WHERE pv.id_product={$this->id} AND pv.figure = 'Y' ");

            if ($emptyProduct) {
                return $ret;
            }
        }

        sqlQuery("UPDATE products SET figure='O' WHERE id={$this->id}");

        $ret++;

        return $ret;
    }

    public function delete()
    {
        $img = new Photos('product');

        $SQL = sqlQuery('SELECT ph.id
	           FROM '.getTableName('photos').' AS ph
	           LEFT JOIN '.getTableName('photos-products')." AS php ON ph.id=php.id_photo
	           WHERE php.id_product={$this->id} ");

        foreach ($SQL as $row) {
            $IDph = $row['id'];
            $samePhotoCount = returnSQLResult('SELECT COUNT(*) FROM '.getTableName('photos-products')." php WHERE php.id_photo={$IDph}");
            if ($samePhotoCount <= 1) {
                $img->erasePhoto($IDph);
            }
        }

        // smazat sekce
        sqlQuery('DELETE FROM '.getTableName('products-sections')." WHERE id_product={$this->id}", '@');
        // smazat odkazy
        sqlQuery('DELETE FROM '.getTableName('links')." WHERE id_product={$this->id}", '@');
        // smazat prilohy
        sqlQuery('DELETE FROM '.getTableName('attachments')." WHERE id_product={$this->id}", '@');
        // smazat souvisejici zbozi
        sqlQuery('DELETE FROM '.getTableName('products-related')." WHERE id_top_product={$this->id}", '@');
        // smazat zbozi
        sqlQuery('DELETE FROM '.getTableName('products')." WHERE id={$this->id}", '@');
    }

    public function checkCodeUnique()
    {
        if (returnSQLResult('SELECT COUNT(*) FROM '.getTableName('products')." p WHERE p.code LIKE '{$this->code}' AND p.id!='{$this->id}'") == 0) {
            return true;
        }

        // Strip "(1)"
        $code = preg_replace('/ \([0-9]+\)$/', '', $this->code);

        $index = 1;
        while (returnSQLResult('SELECT COUNT(*) FROM '.getTableName('products')." p WHERE p.code LIKE '{$code} ({$index})' AND p.id!='{$this->id}'") > 0) {
            $index++;
        }

        $this->code = "{$code} ({$index})";

        return false;
    }

    public function getInStoreOffset($variation_id = null)
    {
        $query = 'SELECT SUM(oi.pieces)
   	   FROM '.getTableName('order_items').' oi
   	   LEFT JOIN '.getTableName('orders').' o ON oi.id_order=o.id,
   	   '.getTableName('orders')." o2
   	   WHERE o2.id={$_GET['IDo']} AND o.date_created >= o2.date_created AND oi.id_product={$this->id}
   	   AND o.status_storno=0 AND o.status IN (".join(',', getStatuses('active')).')';

        if (!empty($variation_id)) {
            $query .= "AND oi.id_variation={$variation_id}";
        }

        return intval(returnSQLResult($query));
    }

    public function getInStoreSuppliers($id_variation = null)
    {
        if ((!findModule(\Modules::PRODUCTS_SUPPLIERS) && !findModule(\Modules::SUPPLIERS)) || !SuppliersUtil::isSuppliersStoreEnabled()) {
            return 0;
        }

        if (isset($this->in_store_suppliers) && ($id_variation == $this->variationId)) {
            return $this->in_store_suppliers;
        }

        $old = $this->in_store_suppliers ?? null;
        $this->in_store_suppliers = null;

        $multiFetch = ServiceContainer::getService(MultiFetch::class);
        $productListId = $this->id;
        if ($id_variation) {
            $productListId .= '/'.$id_variation;
        }
        $productCollection = new ProductCollection([$productListId => $this]);
        if ($id_variation) {
            $productCollection->setEntityType(FilterParams::ENTITY_VARIATION);
        }
        $multiFetch->fetchProductOfSuppliersInStore($productCollection);

        $result = $this->in_store_suppliers;

        if ($id_variation != $this->variationId) {
            $this->in_store_suppliers = $old;
        }

        return $result;
    }

    public function getSuppliers($id_variation = null)
    {
        if (!findModule(\Modules::PRODUCTS_SUPPLIERS) && !findModule(\Modules::SUPPLIERS)) {
            return [];
        }

        $query = 'SELECT pos.id_variation, pos.in_store, pos.id_supplier
               FROM '.getTableName('products_of_suppliers').' pos
               WHERE pos.id_product=:id';

        if (!empty($id_variation)) {
            $query .= ' AND pos.id_variation=:id_variation';
        }

        $SQL = sqlQuery($query, ['id' => $this->id, 'id_variation' => $id_variation]);

        $suppliers = [];
        foreach ($SQL as $row) {
            $variation = $row['id_variation'] ?: '0';
            $suppliers[$variation][$row['id_supplier']] = $row['in_store'];
        }

        return $suppliers;
    }

    public static function isValid($productId, $variationId)
    {
        $where = selectQueryCreate(['p.id' => $productId, 'pv.id' => $variationId], true);
        $count = returnSQLResult('SELECT COUNT(*)
	         FROM '.getTableName('products').' p
	         LEFT JOIN '.getTableName('products_variations')." pv ON pv.id_product = p.id
	         WHERE {$where}");

        return $count == 1;
    }

    public static function automaticUpdateFlagForNewProducts($id_product = false)
    {
        $cfg = Config::get();
        $dbcfg = Settings::getDefault();

        if (!empty($dbcfg['prod_flag_automatic_new']) && !empty($cfg['Products']['Flags']['N'])) {
            $field = empty($dbcfg['prod_flag_automatic_new_field']) ? 'date_added' : $dbcfg['prod_flag_automatic_new_field'];

            $where = ($id_product) ? ' id=:id AND' : '';
            $where .= " NOT FIND_IN_SET('MN', campaign) AND";

            sqlQuery("UPDATE products SET campaign = ADD_TO_SET('N', campaign) WHERE {$where} NOT FIND_IN_SET('N', campaign) AND DATE_ADD({$field}, INTERVAL :days DAY) >  CURDATE()", ['id' => $id_product, 'days' => $dbcfg['prod_flag_automatic_new']]);

            sqlQuery("UPDATE products SET campaign = REMOVE_FROM_SET('N', campaign) WHERE {$where} FIND_IN_SET('N', campaign) AND DATE_ADD({$field}, INTERVAL :days DAY) <  CURDATE() ", ['id' => $id_product, 'days' => $dbcfg['prod_flag_automatic_new']]);
        }
    }

    public static function getName($id)
    {
        return returnSQLResult('SELECT title FROM products WHERE id=:id', ['id' => $id]);
    }

    /**
     * @return string
     */
    public function getTableFields()
    {
        $dbCfg = Settings::getDefault();

        $fields = "p.id, pv.id as variation_id, p.title, p.code, p.ean, p.short_descr, p.long_descr, p.parameters,
            MIN(COALESCE(pv.price, p.price)) as price, MAX(COALESCE(pv.price, p.price)) as priceMax, p.price as priceRaw, p.price_common, p.discount,
				p.producer, p.guarantee, p.delivery_time, DATE_FORMAT(p.updated, '{$dbCfg->date_format}') AS d_update, (pv.id IS NOT NULL) as has_variations,
				p.campaign, p.figure, p.data, p.width, p.height, p.depth, p.code as productCode, COALESCE(pv.date_added, p.date_added) as date_added, GROUP_CONCAT(pv.id) as variationsIds, p.show_in_search";

        if (findModule(\Modules::BONUS_PROGRAM)) {
            $fields .= ', p.bonus_points';
        }

        if (findModule(Modules::PRODUCTS, Modules::SUB_DESCR_PLUS)) {
            $fields .= ',p.id_block';
        }

        if (findModule('products', 'note')) {
            $fields .= ', COALESCE(pv.note, p.note) note_';
        }

        if (findModule(Modules::STOCK_IN)) {
            $fields .= ', p.date_stock_in';
        }

        if (findModule(Modules::PRODUCTS_SERIAL_NUMBERS)) {
            $fields .= ', p.serial_number_require';
        }

        return $fields;
    }

    /* dalsi field_ methody nepridavat do class Product, ale do ProductWrapper */

    public function field_productPrice()
    {
        return PriceWrapper::wrap($this->productPrice);
    }

    public function field_price_array()
    {
        if (empty($this->price_array)) { // SUB_MODERN_PRICES
            return PriceWrapper::wrap($this->productPrice);
        }

        return $this->price_array;
    }

    public function field_price_buy()
    {
        if ($this->price_buy instanceof ProductPrice) {
            return PriceWrapper::wrap($this->price_buy);
        }

        return $this->price_buy;
    }

    public function field_priceCommon()
    {
        if ($this->priceCommon instanceof ProductPrice) {
            return PriceWrapper::wrap($this->priceCommon);
        }

        return $this->priceCommon;
    }

    public function field_priceMax()
    {
        if ($this->priceMax instanceof ProductPrice) {
            return PriceWrapper::wrap($this->priceMax);
        }

        return $this->priceMax;
    }

    public function field_originalPrice()
    {
        return PriceWrapper::wrap($this->productPrice->getOriginalPrice());
    }

    public function field_bonus_points()
    {
        return $this->getBonusPoints($this->variationId);
    }

    public function field_producerTitle()
    {
        return $this->producer['name'] ?? null;
    }

    public function field_producerImage()
    {
        return $this->producer['photo'] ?? null;
    }

    public function field_weight()
    {
        if (!empty($this->weight)) {
            return $this->weight;
        }

        if (findModule(\Modules::PRODUCTS, \Modules::SUB_WEIGHT)) {
            $this->weight = 0.;
            foreach ($this->fetchSets() as $set_product) {
                $this->weight += ($set_product->weight ?? 0) * $set_product->set_pieces;
            }
        }

        return $this->weight;
    }

    /* dalsi field_ methody nepridavat do class Product, ale do ProductWrapper */

    /**
     * @return string
     */
    public function offsetMethod($offset)
    {
        return 'field_'.$offset;
    }

    /**
     * Implements ArrayAccess interface.
     */
    public function offsetSet($offset, $value): void
    {
        $this->{$offset} = $value;
    }

    public function offsetExists($offset): bool
    {
        return isset($this->{$offset});
    }

    public function offsetUnset($offset): void
    {
        unset($this->{$offset});
    }

    public function offsetGet($offset): mixed
    {
        $method = $this->offsetMethod($offset);
        if (method_exists($this, $method)) {
            $res = call_user_func([$this, $method]);

            return $res;
        }

        return isset($this->{$offset}) ? $this->{$offset} : null;
    }

    /**
     * @return \Doctrine\DBAL\Query\QueryBuilder
     */
    protected function createQueryBuilder()
    {
        $qb = sqlQueryBuilder()
            ->select($this->getTableFields())
            ->addSelect(\Query\Product::withVat())
            ->fromProducts()
            ->joinVariationsOnProducts()
            ->groupBy('p.id')
            ->setMaxResults(1);

        $inStoreField = \Query\Product::getInStoreField(false, $qb);

        if (findModule('products', 'showMax')) {
            $qb->addSelect('LEAST('.$inStoreField.', COALESCE(p.in_store_show_max, '.findModule('products', 'showMax').')) in_store');
        } else {
            $qb->addSelect($inStoreField.' in_store');
        }

        if (findModule('seo')) {
            $qb->addSelect('p.meta_title, p.meta_description, p.meta_keywords');
        }

        if (findModule(\Modules::PRODUCTS, \Modules::SUB_WEIGHT)) {
            $qb->addSelect('p.weight');
        }

        if (findModule(\Modules::PRODUCTS, \Modules::SUB_PRICE_BUY)) {
            $qb->addSelect('p.price_buy');
        }

        if (findModule(\Modules::PRICELISTS)) {
            $pricelistContext = ServiceContainer::getService(\KupShop\PricelistBundle\Context\PricelistContext::class);

            if ($pricelistContext->getActiveId()) {
                $qb->andWhere(\KupShop\PricelistBundle\Query\Product::applyPricelist($pricelistContext->getActiveId(), $this->variationId ?? null));
            }
        }

        $qb->andWhere(
            Query\Translation::coalesceTranslatedFields(
                \KupShop\I18nBundle\Translations\ProductsTranslation::class
            )
        );

        if (findModule(Modules::PRODUCTS, Modules::SUB_UNITS)) {
            $qb->leftJoin('p', 'products_units', 'pu', 'p.unit=pu.id')
                ->addSelect('p.unit, pu.short_name, pu.short_name_admin, pu.long_name as unit_long_name')
                ->andWhere(Translation::coalesceTranslatedFields(ProductsUnitsTranslation::class));
        }

        if (findModule(Modules::PRODUCTS, Modules::SUB_UNITS_FLOAT)) {
            $qb->addSelect('pu.pieces_precision');
        }

        if (findModule(Modules::OSS_VATS)) {
            $qb->addSelect('p.id_cn');
        }

        if (findModule(\Modules::PRICE_HISTORY)) {
            $qb->addSelect('p.price_for_discount');
        }

        if (findModule(\Modules::COMPONENTS)) {
            $qb->addSelect(\Query\Product::withProductPhotoId());
        }

        return $qb;
    }

    /**
     * Bacha! Může vracet i null - například ve fulltextu, kde se volá createFromArray, ale není selectnutý field price.
     *
     * @return ProductPrice|null
     */
    public function getProductPrice()
    {
        return $this->productPrice;
    }

    public function setProductPrice($productPrice): void
    {
        $this->productPrice = $productPrice;
    }

    public function fetchBonusPoints(): void
    {
        if (!findModule(Modules::BONUS_PROGRAM)) {
            return;
        }

        $multiFetch = ServiceContainer::getService(MultiFetch::class);
        $multiFetch->fetchBonusPoints(new ProductCollection([$this->id => $this]));
    }

    protected function calculateBonusPoints($id_variation = null): Decimal
    {
        $bonusComputer = ServiceContainer::getService(BonusComputer::class);

        return $bonusComputer->getProductsBonusPoints($this, $id_variation);
    }

    public function getBonusPoints($id_variation = null): ?Decimal
    {
        if (!findModule(Modules::BONUS_PROGRAM)) {
            return null;
        }

        $id_variation = $id_variation ?: $this->matched_id_variation ?: null;

        if (isset($this->bonus_points_cache[$id_variation])) {
            return $this->bonus_points_cache[$id_variation];
        }

        if ($id_variation) {
            $variation = $this->findVariation($id_variation);
            if (isset($variation['bonus_points'])) { // rucni nastaveni bodu primo u varianty
                return $this->bonus_points_cache[$id_variation] = toDecimal($variation['bonus_points']);
            }
        }

        if ($this->bonus_points) { // rucni nastaveni bodu primo u produktu
            return $this->bonus_points_cache[$id_variation] = $this->bonus_points;
        }

        return $this->bonus_points_cache[$id_variation] = $this->calculateBonusPoints($id_variation);
    }

    private function fetchUnit($data)
    {
        if (empty($data['unit'])) {
            $this->unit = [];

            return;
        }

        $this->unit = [
            'id' => $data['unit'],
            'short_name' => $data['short_name'],
            'short_name_admin' => $data['short_name_admin'] ?? $data['short_name'],
            'long_name' => $data['unit_long_name'],
        ];

        if (findModule(Modules::PRODUCTS, Modules::SUB_UNITS_FLOAT)) {
            switch ($data['pieces_precision']) {
                case 0.0001: $this->unit['pieces_precision'] = 4;
                    break;
                case 0.001: $this->unit['pieces_precision'] = 3;
                    break;
                case 0.01: $this->unit['pieces_precision'] = 2;
                    break;
                case 0.1: $this->unit['pieces_precision'] = 1;
                    break;
                case 0:
                default: $this->unit['pieces_precision'] = 0;
            }

            $this->unit['step'] = $data['pieces_precision'];
        }
    }

    /**
     * @return array|mixed
     */
    public function getData()
    {
        if (!$this->data) {
            return [];
        }

        return json_decode($this->data, true);
    }

    /**
     * @return bool
     */
    public function isVirtual()
    {
        $generate_coupon = ($this->getData()['generate_coupon'] ?? 'N');
        if ($generate_coupon == 'Y') {
            return true;
        }

        return false;
    }

    /**
     * @return string
     */
    public function getCN()
    {
        return $this->id_cn ?? Settings::getDefault()['oss_vats']['default'] ?? '';
    }
}

if (empty($subclass)) {
    class Product extends ProductBase
    {
    }
}
