<?php

declare(strict_types=1);

namespace KupShop\CatalogElasticBundle\Util;

use KupShop\CatalogBundle\Entity\Section;
use KupShop\CatalogBundle\Entity\Wrapper\ProductPricesResultWrapper;
use KupShop\CatalogBundle\Enum\InStore;
use KupShop\CatalogBundle\Parameters\ParameterType;
use KupShop\CatalogBundle\ProductList\ProductCollection;
use KupShop\CatalogBundle\Search\ElasticUpdateGroup;
use KupShop\CatalogBundle\Util\ElasticUpdateChangesTrait;
use KupShop\CatalogBundle\Util\Product\ProductDiscountCalculator;
use KupShop\ConvertorsBundle\Utils\ConvertorsUtil;
use KupShop\KupShopBundle\Context\UserContext;
use KupShop\KupShopBundle\Util\ArrayUtil;
use KupShop\KupShopBundle\Util\Contexts;
use Query\Operator;
use Symfony\Contracts\Service\Attribute\Required;

class ProductIndexExtension
{
    use ElasticUpdateChangesTrait;
    protected ProductDiscountCalculator $productDiscountCalculator;

    public function applyFulltextProductListModifiers(\ProductList $productList): void
    {
        $productList->fetchProductOfSuppliersInStore()
            ->fetchProductLabels()
            ->fetchStoresInStore()
            ->fetchVariations(true);

        $productList->addResultModifiers(function (ProductCollection $products) {
            $originalType = Contexts::get(UserContext::class)->getOriginalPriceType();
            $originalType->prefetchProductCollection($products);

            $productIds = $products->getProductIds();

            $positionsQuery = sqlQueryBuilder()
                ->select('psp.*')
                ->from('products_in_sections_positions', 'psp')
                ->andWhere(Operator::inIntArray($productIds, 'psp.id_product'))
                ->addGroupBy('psp.id_product');

            foreach ($positionsQuery->execute()->fetchAllAssociative() as $position) {
                $productId = $position['id_product'];

                if (!isset($products[$productId]->section_positions, $products[$productId]->producer_positions)) {
                    $products[$productId]->section_positions = [];
                    $products[$productId]->producer_positions = [];
                }

                if (!isset($position['position'])) {
                    continue;
                }

                if ($section = $position['id_section']) {
                    $products[$productId]->section_positions[$section] = $position['position'];
                }

                if ($producer = $position['id_producer']) {
                    $products[$productId]->producer_positions[$producer] = $position['position'];
                }
            }

            foreach ($products as $product) {
                if (empty($product->section_positions)) {
                    $product->section_positions = (object) [];
                }

                if (empty($product->producer_positions)) {
                    $product->producer_positions = (object) [];
                }
            }

            $qb = sqlQueryBuilder()
                ->select(
                    'p.id',
                    'FLOOR(UNIX_TIMESTAMP(p.date_added)) AS date_added_ts',
                    'FLOOR(UNIX_TIMESTAMP(p.updated)) AS date_updated_ts',
                    'p.pieces_sold',
                    'p.position',
                    'p.in_store',
                    'p.price'
                )
                ->fromProducts()
                ->andWhere(Operator::inIntArray($productIds, 'p.id'));

            if (findModule(\Modules::STOCK_IN)) {
                $qb->addSelect('FLOOR(UNIX_TIMESTAMP(p.date_stock_in)) AS date_stock_in_ts');
            }

            $discountFieldDefinition = $originalType->getDiscountFieldDefinition();

            $qb->addSelect("({$discountFieldDefinition->getField()}) AS sort_discount")
                ->andWhere($discountFieldDefinition->getSpec());

            $fields = $qb->execute()->fetchAllAssociativeIndexed();

            foreach ($products as $product) {
                if (!isset($fields[$product->id])) {
                    continue;
                }

                foreach ($fields[$product->id] as $field => $value) {
                    $product->{$field} = $value;
                }
            }
        });

        if (findModule(\Modules::SUPPLIERS)) {
            $productList->addResultModifiers($this->fetchVariationsInStoreSuppliers(...));
        }

        if (findModule(\Modules::CONVERTORS)) {
            $productList->addResultModifiers($this->fetchConvertors(...));
        }
    }

    private function fetchVariationsInStoreSuppliers(ProductCollection $products): void
    {
        $data = sqlQueryBuilder()
            ->select('id_variation', 'SUM(in_store) as in_store')
            ->from('products_of_suppliers')
            ->andWhere('id_variation IS NOT NULL')
            ->andWhere(Operator::inIntArray($products->getProductIds(), 'id_product'))
            ->groupBy('id_variation')
            ->execute()->fetchAllKeyValue();

        foreach ($products as $product) {
            if (empty($product->variations)) {
                continue;
            }

            foreach ($product->variations as $variationId => &$variation) {
                $variationInStore = $data[$variationId] ?? 0;

                $variation['in_store_suppliers'] = (int) $variationInStore;
            }
        }
    }

    private function fetchConvertors(ProductCollection $products): void
    {
        $productIds = $products->getProductIds();

        $variationsQuery = sqlQueryBuilder()
            ->select('ctc.id_product, ctc.id_convertor, ctv.id_parameter_value')
            ->fromProducts()
            ->joinVariationsOnProducts()
            ->andWhere(Operator::inIntArray($productIds, 'p.id'));

        ConvertorsUtil::addProductsConvertorsValues($variationsQuery);

        foreach ($variationsQuery->execute() as $row) {
            $product = $products[$row['id_product']];

            if (!isset($product->convertors_parameters)) {
                $product->convertors_parameters = [];
            }

            $product->convertors_parameters[$row['id_convertor']][] = $row['id_parameter_value'];
        }

        $parametersQuery = sqlQueryBuilder()->select('pp.id_product, pp.value_list, cdp.id_convertor')
            ->from('parameters_products', 'pp')
            ->innerJoin('pp', 'convertors_definition_parameters', 'cdp', 'cdp.id_parameter = pp.id_parameter')
            ->andWhere(Operator::inIntArray($productIds, 'id_product'));

        foreach ($parametersQuery->execute() as $row) {
            $product = $products[$row['id_product']];

            if (!isset($product->convertors_parameters)) {
                $product->convertors_parameters = [];
            }

            $product->convertors_parameters[$row['id_convertor']][] = $row['value_list'];
        }
    }

    public function extractIndexFields(\Product $product): array
    {
        $row['date_added'] = $product->date_added_ts ?? time();
        $row['date_updated'] = $product->date_updated_ts ?? time();
        $row['date_stock_in'] = $product->date_stock_in_ts ?? null;
        $row['filter_in_sale'] = $product->discount->isPositive();
        $row['sort_discount'] = $product->sort_discount ?? 0;
        $row['sort_position_in_sections'] = (object) $product->section_positions;
        $row['sort_position_in_producers'] = (object) $product->producer_positions;

        $row['in_store'] = match (true) {
            $product->inStore > 0 => InStore::InStore,
            $product->in_store_suppliers > 0 => InStore::InStoreSupplier,
            default => InStore::NotInStore,
        };

        $row['variations_in_store'] = [];
        $row['variations_data'] = [];
        foreach (($product->variations ?? []) as $variation) {
            $varCombination = [];

            $inStore = $variation['in_store_variation'] ?? $variation['in_store'] ?? 0;

            $inStoreValue = match (true) {
                $inStore > 0 => InStore::InStore,
                ($variation['in_store_suppliers'] ?? 0) > 0 => InStore::InStoreSupplier,
                default => InStore::NotInStore,
            };

            foreach ($variation['combination'] as $labelId => $combination) {
                $row['variations_in_store'][] = [
                    'id' => (int) $combination['id'],
                    'id_label' => (int) $labelId,
                    'in_store' => $inStoreValue,
                ];

                $varCombination[(int) $labelId] = (int) $combination['id'];
            }

            $row['variations_data'][] = ['id' => $variation['id'],  'combination' => $varCombination, 'in_store' => $inStoreValue];
        }

        $row['filter_show_in_search'] = $product->showInSearch === 'Y';

        if ($this->updateItemChanges($product->id, [ElasticUpdateGroup::Price])) {
            /** @var ProductPricesResultWrapper $discount */
            $discount = $this->productDiscountCalculator->calculateForProduct($product);

            $row['price_original'] = $discount->field_priceOriginal()?->field_price_with_vat();
            $row['discount'] = $discount['discount'];
        }

        $row['delivery_time_index'] = $product->deliveryTime;
        $row['delivery_time_text'] = $product->deliveryTimeText;

        // Filtering
        if ($this->updateItemChanges($product->id, ElasticUpdateGroup::Sections)) {
            $row['filter_section'] = array_map(fn (Section $x) => $x->getId(), $product->fetchSections());

            $visibleSections = [];
            foreach ($product->fetchSections() as $section) {
                $visibleSections[] = $section->getId();

                foreach ($section->getParents() as $parent) {
                    if (!$parent->displayProductsFromSubsections()) {
                        continue;
                    }
                    $visibleSections[] = $parent->getId();
                }
            }

            $row['filter_section_recursive'] = array_values(array_unique($visibleSections, SORT_REGULAR));
        }

        if (!empty($product->labels)) {
            // Štítky mohou mít jako klíč stringový "label"
            $row['filter_labels'] = array_column($product->labels, 'id');
        } else {
            $row['filter_labels'] = [];
        }

        if ($this->updateItemChanges($product->id, ElasticUpdateGroup::Variations)) {
            // TODO(hauschwitz): Humus, dělá to tak 4 dotazy pro každej produkt - pryč, přepsat
            $row['filter_variations'] = [];
            $variations = \Variations::getProductLabels((int) $product->id);
            foreach ($variations as $labelId => $variation) {
                $row['filter_variations'][$labelId] = array_keys($variation['values']);
            }
        }

        if ($this->updateItemChanges($product->id, ElasticUpdateGroup::Producer)) {
            $producer = $product->fetchProducer();
            $row['filter_producer'] = (object) ArrayUtil::pick(
                $producer ?? [],
                ['id', 'name', 'photo', 'filter_url'],
            );
        }

        $parameters = [
            ParameterType::FLOAT => [],
            ParameterType::STRING => [],
            ParameterType::LIST => [],
        ];

        if ($this->updateItemChanges($product->id, ElasticUpdateGroup::Parameters)) {
            foreach ($product->fetchParameters() as $parameter) {
                $parameters[$parameter['value_type']][$parameter['id']] = match ($parameter['value_type']) {
                    ParameterType::FLOAT => array_map(function (array $parameterValue) use ($parameter) {
                        return [
                            'id' => $parameter['id'],
                            'value' => $parameterValue['value'],
                        ];
                    }, $parameter['values']),

                    ParameterType::LIST => array_keys($parameter['values']),
                    default => array_column($parameter['values'], 'value'),
                };
            }

            foreach ($parameters as $type => $parameterValues) {
                if ($type === ParameterType::FLOAT) {
                    $row["filter_parameters_{$type}"] = array_merge(...array_values($parameterValues));
                } else {
                    $row["filter_parameters_{$type}"] = $parameterValues;
                }
            }
        }

        $storesInStore = [];
        foreach (($product->storesInStore ?? []) as $idStore => $store) {
            $storesInStore[] = [
                'id' => (int) $idStore,
                'in_store' => ((int) $store['in_store']) > 0
                    ? InStore::InStore
                    : InStore::NotInStore,
            ];
        }

        $row['filter_stores_in_store'] = $storesInStore;

        $row['filter_campaigns'] = array_keys($product->campaign_codes);
        $row['filter_convertors_parameters'] = $product->convertors_parameters ?? [];

        // from updateProductsAttributes
        $row['position'] = max(0, min(10, 5 - (($product->position ?? POSITION_STANDARD) - POSITION_STANDARD) / 1000));
        $row['sort_position'] = $product->position ?? POSITION_STANDARD;

        $row['sold'] = max(0, min(10, intval(log10($product->pieces_sold ?? 0) * 3)));

        // clip to int32
        $row['store_value'] = max(-2147483648,
            min(2147483647,
                $product->discount->asFloat() > 0 ? ($product->in_store ?? 0) * (toDecimal($product->price)->addDiscount($product->discount)->asFloat())
                    : ($product->in_store ?? 0) * $product->price));

        return $row;
    }

    #[Required]
    final public function setProductDiscountCalculator(ProductDiscountCalculator $productDiscountCalculator): void
    {
        $this->productDiscountCalculator = $productDiscountCalculator;
    }
}
