<?php

declare(strict_types=1);

namespace KupShop\CatalogElasticBundle\Filters;

use Doctrine\DBAL\Exception as DBALException;
use KupShop\CatalogBundle\Parameters\ParameterType;
use KupShop\CatalogBundle\ProductList\DynamicFilterAttributes;
use KupShop\CatalogElasticBundle\DataContext\ElasticFilterSpecContext;
use KupShop\CatalogElasticBundle\DataContext\ElasticFilterValuesContext;
use KupShop\CatalogElasticBundle\Elastica\QueryBuilder;
use KupShop\CatalogElasticBundle\Enum\FilterValuesOrder;
use KupShop\I18nBundle\Translations\ParametersListTranslation;
use Query\Operator;
use Query\Translation;

class Parameters extends ElasticFilter
{
    use QueryBuilder;

    public function getAggregations(DynamicFilterAttributes $attributes): iterable
    {
        if (!$attributes->parameters) {
            return yield from [];
        }

        foreach ($attributes->parameters as $parameterId => $parameter) {
            $type = $parameter['value_type'];
            $aggName = "parameters.{$parameterId}";

            if ($type === ParameterType::FLOAT) {
                // filter_parameters_float jsou uložené jako nested
                // tzn. [{"id": "5", "value": 1}, {"id": 6, "value": 2}, ...]
                /** {@see FulltextElastic:83} */
                $fieldName = self::getElasticFieldName(ParameterType::FLOAT);

                $filterSubAgg = $this->agg()->filter('filter', $this->query()->term(["{$fieldName}.id" => $parameterId]));
                $statsAgg = $this->agg()->stats('value')->setField("{$fieldName}.value");

                yield $this->agg()
                    ->nested($aggName, $fieldName)
                    ->addAggregation($filterSubAgg->addAggregation($statsAgg));

                continue;
            }

            yield $this->agg()
                ->terms($aggName)
                ->setField(self::getElasticFieldName($type, $parameterId));
        }
    }

    public function applyFilters(ElasticFilterSpecContext $ctx): void
    {
        $withoutId = $ctx->getWithoutId();

        foreach ($ctx->value as $parameterId => $parameter) {
            if ($parameterId == $withoutId) {
                continue;
            }

            if ($parameter['type'] === ParameterType::FLOAT) {
                $fieldName = self::getElasticFieldName(ParameterType::FLOAT);

                $rangeParams = [];
                if (($min = $parameter['values']['min'] ?? null) !== null) {
                    $rangeParams['gte'] = $min;
                }

                if (($max = $parameter['values']['max'] ?? null) !== null) {
                    $rangeParams['lte'] = $max;
                }

                $filter = $this->query()->nested()
                    ->setPath($fieldName)
                    ->setQuery(
                        $this->query()->bool()
                            ->addFilter($this->query()->term(["{$fieldName}.id" => $parameterId]))
                            ->addFilter($this->query()->range("{$fieldName}.value", $rangeParams)),
                    );
            } else {
                $filter = $this->query()->terms(
                    self::getElasticFieldName($parameter['type'], $parameterId),
                    $parameter['values'],
                );
            }

            $ctx->set("parameters.{$parameterId}", $filter, 'parameters');
        }
    }

    public function collectValues(ElasticFilterValuesContext $ctx): void
    {
        $aggregations = $ctx->getTargetAggregations();
        if (empty($aggregations)) {
            return;
        }

        $listValuesIds = [];
        foreach ($aggregations as $agg) {
            if (!$agg->isBucketAggregation()) {
                continue;
            }

            foreach ($agg->getBuckets() as $bucket) {
                if (!isset($bucket['key']) || !is_numeric($bucket['key'])) {
                    continue;
                }

                $listValuesIds[] = $bucket['key'];
            }
        }

        $listParameterValues = $this->fetchParameterValues($listValuesIds);
        $isFiltered = !$ctx->isFilterEmpty();
        $parameters = [];

        foreach ($aggregations as $parameterId => $parameterBucketOrStats) {
            $isParameterAllowedInSection = array_key_exists($parameterId, $ctx->filterAttributes->parameters);
            if (!$isParameterAllowedInSection) {
                continue;
            }

            $parameter = $ctx->filterAttributes->parameters[$parameterId];
            $parameterType = $parameter['value_type'];
            if ($parameterType === ParameterType::STRING) {
                continue;
            }

            // Fill cache with empty values (to not fetch from DB) when no product with current parameter is found in section
            $isEmptyFloatAgg = $parameterType === ParameterType::FLOAT
                && ($parameterBucketOrStats['filter']['doc_count'] ?? 0) === 0;

            $isEmptyListAgg = $parameterType === ParameterType::LIST
                && empty($parameterBucketOrStats['buckets']);

            if (!$isFiltered && ($isEmptyListAgg || $isEmptyFloatAgg)) {
                $parameters[$parameterId] = [
                    'param_name' => null,
                    'values' => [],
                ];

                continue;
            }

            if ($parameterType === ParameterType::FLOAT) {
                $stats = $parameterBucketOrStats['filter']['value'];

                $parameters[$parameterId] = array_merge($parameter, [
                    'param_name' => $parameter['name'],
                    'values' => $stats,
                ]);

                continue;
            }

            // map of parameterId => product count
            $valueCounts = [];
            foreach ($parameterBucketOrStats['buckets'] as $bucket) {
                $valueCounts[$bucket['key']] = $bucket['doc_count'];
            }

            $parameterValues = match ($ctx->getOrderBy($parameterId)) {
                // The results from db ($listParameterValues) are ordered according to admin (position)
                FilterValuesOrder::Admin => array_map(
                    static fn (array $dbValue) => array_merge(
                        $dbValue,
                        ['count' => $valueCounts[$dbValue['id']] ?? 0],
                    ),
                    $listParameterValues[$parameterId] ?? [],
                ),

                FilterValuesOrder::ProductsSold => array_filter(array_map(
                    static function (array $esValue) use ($listParameterValues, $parameterId) {
                        if (!isset($listParameterValues[$parameterId][$esValue['key']])) {
                            return null;
                        }

                        return array_merge(
                            $listParameterValues[$parameterId][$esValue['key']],
                            ['count' => $esValue['doc_count']],
                        );
                    },
                    $parameterBucketOrStats['buckets'],
                )),
            };

            foreach ($parameterValues as $key => &$parameterValue) {
                if (!$isFiltered && $parameterValue['count'] == 0) {
                    unset($parameterValues[$key]);
                    continue;
                }

                $parameterValue['param_name'] = $parameter['name'];
                $parameterValue['unit'] = $parameter['unit'];

                if ($parameterType === ParameterType::LIST && is_string($parameterValue['data'] ?? null)) {
                    $parameterValue['data'] = json_decode($parameterValue['data'], true);
                }
            }

            $parameters[$parameterId] = array_merge($parameter, [
                'param_name' => $parameter['name'],
                'values' => $parameterValues,
                'values_count' => $parameterBucketOrStats->getTotalBucketCount(),
            ]);
        }

        $ctx->set('parameters', $parameters);
    }

    /**
     * @throws DBALException
     */
    private function fetchParameterValues(array $parameterValueIds): array
    {
        $translateFields = ['value' => 'name', 'description' => 'description'];
        if (findModule(\Modules::INDEXED_FILTER)) {
            $translateFields['filter_url'] = 'filter_url';
        }

        $query = sqlQueryBuilder()
            ->select('pl.id_parameter', 'pl.id', 'pl.data')
            ->from('parameters_list', 'pl')
            ->orderBy('pl.position')
            ->andWhere(Operator::inIntArray($parameterValueIds, 'pl.id'))
            ->andWhere(Translation::coalesceTranslatedFields(
                ParametersListTranslation::class,
                $translateFields,
            ))
            ->addOrderBy('pl.position');

        $result = [];
        foreach ($query->execute() as $parameterValue) {
            $result[$parameterValue['id_parameter']][$parameterValue['id']] = $parameterValue;
        }

        return $result;
    }

    #[\Override]
    public function supportsOrderBy(): bool
    {
        return true;
    }

    protected static function getElasticFieldName(string $parameterType = ParameterType::LIST, ?int $parameterId = null): string
    {
        return match ($parameterId) {
            null => "filter_parameters_{$parameterType}",
            default => "filter_parameters_{$parameterType}.{$parameterId}",
        };
    }
}
