<?php

namespace App;

use App\Facades\Feature;
use App\Facades\Settings;
use App\Filter\FilterIndex;
use App\Http\Resources\VehicleList;
use App\Modules\PlaceholderImages\ImaginStudio;
use App\Services\MeilisearchService;
use App\Traits\CacheObject;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Pagination\LengthAwarePaginator as Paginator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Log;
use Mtc\Filter\Contracts\FilterObject;
use Mtc\Filter\Contracts\FilterSeoContract;
use Mtc\Filter\Contracts\IsFilter;
use Mtc\MercuryDataModels\Colour;
use Mtc\MercuryDataModels\Services\LocatingService;
use Mtc\MercuryDataModels\Vehicle;
use Mtc\MercuryDataModels\VehicleFinance;
use Mtc\MercuryDataModels\VehicleModel;

class MeilisearchFilter extends Filter
{
    use CacheObject;

    protected MeilisearchService $meilisearch;

    protected array $meilisearchFilters = [];

    protected ?array $cachedSearchResults = null;

    protected ?array $cachedFacets = null;

    protected ?string $geoFilter = null;

    /**
     * Cache for facet distribution per filter type (excludes own filter)
     */
    protected array $facetsByFilterType = [];

    /**
     * Cached range facet data (distribution + stats) for all range filters
     */
    protected ?array $cachedRangeFacets = null;

    /**
     * Configuration for range filters that use Meilisearch facetStats/facetDistribution
     * instead of database queries.
     *
     * type=config: Uses facetStats min/max to filter predefined config ranges
     * type=discrete: Uses facetDistribution keys as distinct values
     */
    protected array $rangeFilterFacetConfig = [
        'price_min' => [
            'attr' => 'price',
            'type' => 'config',
            'config_key' => 'automotive.filter-ranges.price',
            'has_value' => true,
            'slice_last' => true
        ],
        'price_max' => [
            'attr' => 'price',
            'type' => 'config',
            'config_key' => 'automotive.filter-ranges.price',
            'has_value' => true
        ],
        'price_monthly_min' => [
            'attr' => 'monthly_price',
            'type' => 'config',
            'config_key' => 'automotive.filter-ranges.monthly_price',
            'has_value' => true,
            'slice_last' => true
        ],
        'price_monthly_max' => [
            'attr' => 'monthly_price',
            'type' => 'config',
            'config_key' => 'automotive.filter-ranges.monthly_price',
            'has_value' => true
        ],
        'mileage_min' => [
            'attr' => null,
            'type' => 'config',
            'config_key' => 'automotive.filter-ranges.mileage',
            'slice_last' => true
        ],
        'mileage_max' => [
            'attr' => null,
            'type' => 'config',
            'config_key' => 'automotive.filter-ranges.mileage'
        ],
        'engine_size_min' => [
            'attr' => 'engine_size_cc',
            'type' => 'config',
            'config_key' => 'automotive.filter-ranges.engine-size',
            'has_value' => true
        ],
        'engine_size_max' => [
            'attr' => 'engine_size_cc',
            'type' => 'config',
            'config_key' => 'automotive.filter-ranges.engine-size',
            'has_value' => true
        ],
        'co2' => [
            'attr' => 'co2',
            'type' => 'config',
            'config_key' => 'automotive.filter-ranges.co2'
        ],
        'consumption' => [
            'attr' => 'mpg',
            'type' => 'config',
            'config_key' => 'automotive.filter-ranges.mpg',
            'has_value' => true
        ],

        'manufacture_year_min' => [
            'attr' => 'manufacture_year',
            'type' => 'discrete',
            'sort' => 'desc'
        ],
        'manufacture_year_max' => [
            'attr' => 'manufacture_year',
            'type' => 'discrete',
            'sort' => 'desc'
        ],
        'doors_min' => [
            'attr' => 'door_count',
            'type' => 'discrete',
            'sort' => 'asc',
            'min_value' => 1
        ],
        'doors_max' => [
            'attr' => 'door_count',
            'type' => 'discrete',
            'sort' => 'asc',
            'min_value' => 1
        ],
        'seats_min' => [
            'attr' => 'seat_count',
            'type' => 'discrete',
            'sort' => 'asc',
            'min_value' => 1
        ],
        'seats_max' => [
            'attr' => 'seat_count',
            'type' => 'discrete',
            'sort' => 'asc',
            'min_value' => 1
        ],
    ];

    /**
     * Mapping from filter type to Meilisearch facet attribute name
     */
    protected array $facetAttributeMapping = [
        'make' => 'make_slug',
        'model' => 'model_slug',
        'body_type' => 'body_style_slug',
        'fuel_type' => 'fuel_type_slug',
        'transmission' => 'transmission_slug',
        'colour' => 'colour_slug',
        'location' => 'dealership_slug',
        'franchise' => 'franchise_id',
        'vehicle_type' => 'type',
        'vehicle_category' => 'vehicle_category',
        'is_new' => 'is_new',
        'is_sold' => 'is_sold',
        'labels' => 'label_ids',
        'features' => 'feature_ids',
        'stock_status' => 'stock_status',
        'trim' => 'trim_slug',
        'age' => 'manufacture_year',
    ];

    public function __construct(Request $request, FilterObject $product_handler, FilterSeoContract $seo)
    {
        parent::__construct($request, $product_handler, $seo);
        $this->meilisearch = App::make(MeilisearchService::class);
        // Initialize query to avoid "must not be accessed before initialization" errors
        // when parent methods that use $this->query are called
        $this->query = $this->product_handler->createQuery();
    }

    public function handleWidget(): array
    {
        $this->request = request();
        $this->applyFiltersToResults = Settings::get('filter-apply-selections-to-results-for-widget', false);
        $this->selections = $this->groupSelectionsByType($this->request->input('selections') ?? []);
        $this->buildMeilisearchFilters();

        return [
            'result_count' => $this->getResultCount(false),
            'filters' => $this->getFilterResults(true),
            'url' => $this->getPageUrl(),
        ];
    }

    public function handle(): array
    {
        $this->request = request();
        if ($this->request->has('slug')) {
            $this->filter_url_elements = explode('/', $this->request->input('slug'));
            $this->matchSortFromUrlElements();
            $this->matchSelectedFilters();
            $this->matchCustomSelections();
            $this->checkForSearchTerm();
        } else {
            $this->selections = $this->groupSelectionsByType($this->request->input('selections') ?? []);
            $this->matchSortFromAjaxRequest();
        }

        $this->trackSearchTerm();
        $this->buildMeilisearchFilters();
        $this->applySortBasedFilters();

        $results = $this->getResults();

        return [
            'results' => $results,
            'filters' => $this->getCachedFilterResults(),
            'sort_options' => $this->getSortOptions(),
            'sort_by' => $this->active_sort_option_name,
            'selections' => $this->getSelectionList(),
            'url' => $this->getPageUrl(),
            'seo' => $this->fillSeo(),
            'finance_example' => $this->getRepresentativeFinance($results),
            'imagin_studio_base_url' => Feature::isEnabled('imagin-studio-placeholders')
                ? (new ImaginStudio())->getBaseUrl()
                : null,
            'load_more' => [
                'enabled' => Settings::get('automotive-enable_load_more', false),
                'text' => Settings::get('automotive-load_more_text'),
                'previous_text' => Settings::get('automotive-load_more_previous_text'),
            ]
        ];
    }

    protected function buildMeilisearchFilters(): void
    {
        $this->meilisearchFilters = [
            'is_published' => true,
            'is_sold' => false,
        ];

        $enabledFilters = $this->getEnabledFilterNames();

        foreach ($this->selections as $type => $values) {
            if (empty($values)) {
                continue;
            }

            // Handle postcode filter separately as it uses geo search
            if ($type === 'postcode') {
                $this->buildGeoFilter($values);
                continue;
            }

            if (!in_array($type, $enabledFilters, true)) {
                continue;
            }

            $filterMapping = $this->getMeilisearchFilterMapping($type, $values);
            if ($filterMapping !== null) {
                $this->meilisearchFilters = array_replace_recursive($this->meilisearchFilters, $filterMapping);
            }
        }

        // If both sold and available are selected, remove the is_sold filter to show all
        if ($this->isSoldFilterShowsAll()) {
            unset($this->meilisearchFilters['is_sold']);
        }
    }

    protected function buildGeoFilter(array $values): void
    {
        try {
            $postcode = $values[0] ?? null;
            if (!$postcode) {
                return;
            }

            $location = App::make(LocatingService::class)->locate($postcode);
            if ($location->lat() && $location->lng()) {
                $distance = $this->getDistanceSelection();
                // Convert miles to meters (1 mile = 1609.34 meters)
                $distanceMeters = $distance * 1609.34;
                // Cast coordinates to float so they cannot inject into the raw filter expression
                $lat = (float) $location->lat();
                $lng = (float) $location->lng();
                $this->geoFilter = "_geoRadius({$lat}, {$lng}, {$distanceMeters})";
            }
        } catch (\Exception $e) {
            Log::debug('Meilisearch geo filter error: ' . $e->getMessage());
        }
    }

    protected function getDistanceSelection(): int
    {
        $distance = 200; // Default distance in miles

        if (isset($this->selections['distance']) && !empty($this->selections['distance'])) {
            $distance = (int)$this->selections['distance'][0];
        }

        return $distance;
    }

    protected function getEnabledFilterNames(): array
    {
        return $this->getFilters()->keys()->toArray();
    }

    protected function getMeilisearchFilterMapping(string $type, array $values): ?array
    {
        return match ($type) {
            'make' => ['make_slug' => $values],
            'model' => ['model_slug' => $values],
            'body_type' => ['body_style_slug' => $values],
            'fuel_type' => ['fuel_type_slug' => $values],
            'transmission' => ['transmission_slug' => $values],
            'colour' => ['colour_slug' => $values],
            'location' => ['dealership_slug' => $values],
            'franchise' => ['franchise_id' => array_map('intval', $values)],
            'vehicle_type' => ['type' => $values],
            'vehicle_category' => ['vehicle_category' => $values],
            'is_new' => ['is_new' => in_array('new', $values)],
            'is_sold' => ['is_sold' => in_array('sold', $values)],
            'is_vat_applicable' => ['is_vat_applicable' => in_array('true', $values) || in_array(true, $values)],
            'labels' => ['label_ids' => array_map('intval', $values)],
            'features' => ['feature_ids' => array_map('intval', $values)],
            'stock_status' => ['stock_status' => $values],
            'trim' => ['trim_slug' => $values],
            'price_min' => ['price' => ['min' => (int)$values[0]]],
            'price_max' => ['price' => ['max' => (int)$values[0]]],
            'price_monthly_min' => ['monthly_price' => ['min' => (int)$values[0]]],
            'price_monthly_max' => ['monthly_price' => ['max' => (int)$values[0]]],
            'mileage_min' => $this->getMileageFilter('min', $values),
            'mileage_max' => $this->getMileageFilter('max', $values),
            'manufacture_year_min' => ['manufacture_year' => ['min' => (int)$values[0]]],
            'manufacture_year_max' => ['manufacture_year' => ['max' => (int)$values[0]]],
            'engine_size_min' => ['engine_size_cc' => ['min' => (int)$values[0]]],
            'engine_size_max' => ['engine_size_cc' => ['max' => (int)$values[0]]],
            'doors_min' => ['door_count' => ['min' => (int)$values[0]]],
            'doors_max' => ['door_count' => ['max' => (int)$values[0]]],
            'seats_min' => ['seat_count' => ['min' => (int)$values[0]]],
            'seats_max' => ['seat_count' => ['max' => (int)$values[0]]],
            'co2' => ['co2' => ['max' => (int)$values[0]]],
            default => null,
        };
    }

    protected function getMileageFilter(string $bound, array $values): array
    {
        $field = Settings::get('automotive-distance_measurement') === 'mi'
            ? 'odometer_mi'
            : 'odometer_km';

        return [$field => [$bound => (int)$values[0]]];
    }

    protected function getMeilisearchSort(): array
    {
        return match ($this->active_sort_option_name) {
            'relevance' => ['order:asc'],
            'latest' => ['published_at:desc'],
            'oldest' => ['created_at:asc'],
            'price-desc' => ['price:desc'],
            'price-asc' => ['price:asc'],
            'monthly-price-desc' => ['monthly_price:desc'],
            'monthly-price-asc' => ['monthly_price:asc'],
            'mileage-desc' => Settings::get('automotive-distance_measurement') === 'mi'
                ? ['odometer_mi:desc']
                : ['odometer_km:desc'],
            'mileage-asc' => Settings::get('automotive-distance_measurement') === 'mi'
                ? ['odometer_mi:asc']
                : ['odometer_km:asc'],
            'age-desc' => ['manufacture_year:desc'],
            'age-asc' => ['manufacture_year:asc'],
            default => ['created_at:desc'],
        };
    }

    /**
     * Check if the is_sold filter has both sold and available selected,
     * meaning we should show all vehicles regardless of sold status.
     */
    protected function isSoldFilterShowsAll(): bool
    {
        if (!isset($this->selections['is_sold'])) {
            return false;
        }

        $values = $this->selections['is_sold'];

        return in_array('sold', $values) && in_array('available', $values);
    }

    /**
     * Build is_sold filter results from Meilisearch facet distribution.
     * Maps boolean facet values to sold/available options.
     */
    protected function buildIsSoldResultsFromFacets(): Collection
    {
        $facets = $this->getFacetDistributionForFilter('is_sold');
        $facetCounts = $facets['is_sold'] ?? [];

        $soldCount = $facetCounts['true'] ?? $facetCounts[1] ?? 0;
        $availableCount = $facetCounts['false'] ?? $facetCounts[0] ?? 0;

        return collect([
            [
                'id' => 'sold',
                'name' => 'Sold',
                'count' => $soldCount,
                'slug' => 'sold',
            ],
            [
                'id' => 'available',
                'name' => 'Available',
                'count' => $availableCount,
                'slug' => 'available',
            ],
        ]);
    }

    /**
     * Build is_new filter results from Meilisearch facet distribution.
     * Maps boolean facet values to used/new/ex_demo/pre_reg options matching the database filter format.
     */
    protected function buildIsNewResultsFromFacets(): Collection
    {
        $filtersWithoutCurrent = $this->buildMeilisearchFiltersExcluding('is_new');
        $searchTerm = $this->selections['search'][0] ?? null;

        $facets = $this->meilisearch->getFacets(
            $filtersWithoutCurrent,
            ['is_new', 'is_demo'],
            $searchTerm,
            $this->geoFilter
        );

        $isNewCounts = $facets['is_new'] ?? [];
        $isDemoCounts = $facets['is_demo'] ?? [];

        $newCount = $isNewCounts['true'] ?? $isNewCounts[1] ?? 0;
        $notNewCount = $isNewCounts['false'] ?? $isNewCounts[0] ?? 0;
        $demoCount = $isDemoCounts['true'] ?? $isDemoCounts[1] ?? 0;

        $results = collect();

        if ($notNewCount > 0) {
            $results->push([
                'id' => 'used',
                'name' => 'Used',
                'count' => $notNewCount,
                'slug' => 'used',
            ]);
        }

        if ($newCount > 0) {
            $results->push([
                'id' => 'new',
                'name' => 'New',
                'count' => $newCount,
                'slug' => 'new',
            ]);
        }

        if ($demoCount > 0) {
            $results->push([
                'id' => 'ex_demo',
                'name' => 'Ex-Demo',
                'count' => $demoCount,
                'slug' => 'ex_demo',
            ]);
        }

        if (Settings::get('vehicles-pre-reg-condition-enabled')) {
            $results->push([
                'id' => 'pre_reg',
                'name' => 'Pre-Reg',
                'count' => null,
                'slug' => 'pre_reg',
            ]);
        }

        return $results;
    }

    protected function applySortBasedFilters(): void
    {
        // When sorting by monthly price, exclude vehicles without monthly price
        if (in_array($this->active_sort_option_name, ['monthly-price-desc', 'monthly-price-asc'])) {
            $this->meilisearchFilters['monthly_price'] = ['min' => 1];
        }
    }

    protected function getResults(): JsonResource
    {
        $pageLimit = Settings::get('filter-results-per-page', Config::get('filter.result_page_limit', 24));
        $perPage = request('perPage', $pageLimit);
        $page = (int)str_replace('=', '', request('page', 1));
        $offset = ($page - 1) * $perPage;

        $searchTerm = $this->selections['search'][0] ?? null;

        $searchResults = $this->meilisearch->searchVehicles(
            $this->meilisearchFilters,
            $this->getMeilisearchSort(),
            $perPage,
            $offset,
            $searchTerm,
            $this->geoFilter
        );

        $this->cachedSearchResults = $searchResults;

        $vehicleIds = collect($searchResults['hits'])->pluck('id')->toArray();

        if (empty($vehicleIds)) {
            $paginator = new Paginator(collect(), 0, $perPage, $page);
            return new VehicleList($paginator, [], true);
        }

        $vehicles = Vehicle::query()
            ->whereIn('id', $vehicleIds)
            ->withRelationshipsForCardView()
            ->when(Settings::get('automotive-vehicle-brand-on-filter-card'), fn ($query) => $query->with('make'))
            ->get()
            ->sortBy(function ($vehicle) use ($vehicleIds) {
                return array_search($vehicle->id, $vehicleIds);
            })
            ->values();

        $this->applyForResults();
        $this->product_handler->setCurrentQuery(clone $this->query);
        $catalogOffers = $this->product_handler->getCatalogOffers();

        $paginator = new Paginator($vehicles, $searchResults['total'], $perPage, $page);

        return new VehicleList($paginator, $catalogOffers, true);
    }

    protected function getCachedFilterResults(): array
    {
        if (collect($this->selections)->flatten()->count() > 2) {
            return $this->getFilterResults();
        }

        $append = collect($this->selections)
            ->map(fn ($values, $type) => $type . '=' . implode('|', (array) $values))
            ->sort()
            ->values()
            ->implode('-');

        return $this->cache('meilisearch-filter-results' . $append, 15, fn () => $this->getFilterResults());
    }

    protected function getFilterResults(bool $prependAny = false): array
    {
        $input = $this->request->input();
        $filters = $this->getFilters()
            ->when(
                !empty($input['only_filters']),
                fn ($all) => $all->filter(fn ($filter, $name) => in_array($name, $input['only_filters'] ?? [], true))
            )
            ->map(fn (IsFilter $filter, $name) => $filter->format($this->retrieveSingleFilterResults($filter, $name)))
            ->filter()
            ->map(function (array $filterGroup, $type) use ($prependAny) {
                $filterGroup['results'] = collect($filterGroup['results'])
                    ->map(function ($entry) use ($type) {
                        if (is_array($entry)) {
                            $valueField = $type === 'price' ? 'value' : 'id';
                            $entry['selected'] = isset($this->selections[$type])
                                && in_array($entry[$valueField], $this->selections[$type]);
                        }
                        return $entry;
                    })
                    ->when($prependAny, fn ($group) => $group->prepend([
                        'id' => 0,
                        'name' => __('labels.any'),
                    ]))
                    ->values();
                return $filterGroup;
            })
            ->toArray();

        return $this->groupRangeFilters($filters);
    }

    /**
     * Override to use Meilisearch facet counts instead of database queries.
     */
    protected function retrieveSingleFilterResults(IsFilter $filter, string $filter_name): Collection
    {
        $limit = in_array($filter_name, $this->request->input('expanded', []), true)
            ? 0
            : $this->config['filter_limit'];

        // Boolean filters use custom mappings to return proper names
        if ($filter_name === 'is_sold') {
            return $this->buildIsSoldResultsFromFacets();
        }

        if ($filter_name === 'is_new') {
            return $this->buildIsNewResultsFromFacets();
        }

        $facetAttribute = $this->facetAttributeMapping[$filter_name] ?? null;

        // Build results from Meilisearch facets when mapping exists
        if ($facetAttribute) {
            return $this->buildResultsFromFacets($filter_name, $filter, $facetAttribute, $limit);
        }

        // Range filters: use Meilisearch facetStats/facetDistribution instead of DB queries
        if (isset($this->rangeFilterFacetConfig[$filter_name])) {
            return $this->buildRangeResultsFromFacets($filter_name, $filter);
        }

        $results = $filter->getResults(
            function ($query) use ($filter_name) {
                $this->applyForFilters($query, $filter_name);
            },
            $limit,
            $this->selections[$filter_name] ?? []
        );

        return $this->mergeFacetCounts($results, $filter_name, $filter);
    }

    /**
     * Merge Meilisearch facet counts into filter results.
     * Excludes the current filter from the query to show accurate counts for each option.
     * Results are sorted by count descending to show most relevant options first.
     */
    protected function mergeFacetCounts(Collection $results, string $filterName, IsFilter $filter): Collection
    {
        $facetAttribute = $this->facetAttributeMapping[$filterName] ?? null;

        if (!$facetAttribute) {
            return $results;
        }

        // Get facets with current filter excluded so counts reflect "what if I select this option"
        $facets = $this->getFacetDistributionForFilter($filterName);
        $facetCounts = $facets[$facetAttribute] ?? [];

        if (empty($facetCounts)) {
            return $results;
        }

        // Get the ID attribute used by this filter (could be 'slug', 'id', 'colour', etc.)
        $idAttribute = $filter->getIdAttribute();

        $results = $results->map(function ($item) use ($facetCounts, $idAttribute, $facetAttribute, $filterName) {
            // Handle both array and object results
            $isArray = is_array($item);

            // Get the value to match against facet distribution
            $matchValue = $this->getFacetMatchValue($item, $idAttribute, $facetAttribute, $filterName);

            // Get count from Meilisearch facet distribution
            $count = $facetCounts[$matchValue] ?? 0;

            // Set result_count on object or array
            if ($isArray) {
                $item['result_count'] = $count;
                $item['count'] = $count;
            } else {
                $item->result_count = $count;
            }

            return $item;
        });

        // Sort by popularity (result_count) or alphabetically by name based on setting
        if (Settings::get('filter-sort-options-by-popularity', false)) {
            $results = $results->sortByDesc(function ($item) {
                return is_array($item) ? ($item['result_count'] ?? 0) : ($item->result_count ?? 0);
            });
        }

        return $results->values();
    }

    /**
     * Get the value to match against Meilisearch facet distribution.
     */
    protected function getFacetMatchValue($item, string $idAttribute, string $facetAttribute, string $filterName): mixed
    {
        $isArray = is_array($item);

        // Helper to get value from array or object
        $getValue = function (string $key) use ($item, $isArray) {
            if ($isArray) {
                return $item[$key] ?? null;
            }
            return $item->{$key} ?? null;
        };

        // For colour filter with colour mapping, the Colour model has 'colour' attribute
        // but Meilisearch uses slugified colour names in 'colour_slug'
        if ($filterName === 'colour') {
            $colourName = $getValue('colour') ?? $getValue($idAttribute);
            if ($colourName) {
                return \Illuminate\Support\Str::slug($colourName);
            }
        }

        // For slug-based facets, use the slug attribute
        if (str_ends_with($facetAttribute, '_slug')) {
            return $getValue('slug') ?? $getValue($idAttribute);
        }

        // For ID-based facets (like franchise_id, label_ids), use the ID
        if (str_ends_with($facetAttribute, '_id') || str_ends_with($facetAttribute, '_ids')) {
            return $getValue('id') ?? $getValue($idAttribute);
        }

        // For other facets (like 'type', 'manufacture_year'), use the ID attribute value
        return $getValue($idAttribute) ?? $getValue('id');
    }

    /**
     * Build filter option list from Meilisearch facet distribution + filter_index metadata.
     * Avoids querying the vehicles table entirely.
     */
    protected function buildResultsFromFacets(
        string $filterName,
        IsFilter $filter,
        string $facetAttribute,
        int $limit
    ): Collection {
        $facets = $this->getFacetDistributionForFilter($filterName);
        $facetCounts = $facets[$facetAttribute] ?? [];

        if (empty($facetCounts)) {
            return collect();
        }

        $facetKeys = array_keys($facetCounts);
        $filterType = method_exists($filter, 'filterType') ? $filter->filterType() : $filterName;

        $indexEntries = $this->lookupFilterIndex($filterType, $facetKeys, $facetAttribute);

        // Backfill missing entries from the filter's reference model table
        $missingKeys = array_diff(array_map('strval', $facetKeys), $indexEntries->keys()->toArray());
        if (!empty($missingKeys)) {
            $modelClass = method_exists($filter, 'getModel') ? $filter->getModel() : '';
            if ($modelClass && class_exists($modelClass)) {
                try {
                    $lookupField = (str_ends_with($facetAttribute, '_id') || str_ends_with($facetAttribute, '_ids'))
                        ? 'id'
                        : 'slug';
                    $models = $modelClass::query()->whereIn($lookupField, $missingKeys)->get();
                    foreach ($models as $model) {
                        $key = (string) $model->{$lookupField};
                        $entry = new FilterIndex();
                        $entry->slug = $model->slug ?? $key;
                        $entry->name = $model->name ?? $key;
                        $entry->filter_id = $model->id;
                        $entry->filter_type = $filterType;
                        $indexEntries->put($key, $entry);
                    }
                } catch (\Exception $e) {
                    // Model table may not have the expected column (e.g. colours has no slug column)
                }
            }
        }

        $idAttr = $filter->getIdAttribute();
        $nameAttr = $filter->getNameAttribute();

        $results = collect($facetCounts)
            ->map(function ($count, $key) use ($indexEntries, $filter, $filterType, $idAttr, $nameAttr) {
                $entry = $indexEntries->get((string)$key);

                if (!$entry) {
                    $entry = new FilterIndex();
                    $entry->slug = (string)$key;
                    $entry->name = (string)$key;
                    $entry->filter_id = $key;
                    $entry->filter_type = $filterType;
                }

                $entry->id = $entry->filter_id ?? $entry->slug;
                $entry->result_count = $count;
                $entry->count = $count;
                $entry->num_results = $count;

                if ($nameAttr !== 'name') {
                    $entry->{$nameAttr} = $entry->name;
                }

                if ($idAttr !== 'id' && $idAttr !== $nameAttr) {
                    $entry->{$idAttr} = $entry->slug;
                }

                return $entry;
            });

        // Sort by popularity (result_count) or alphabetically by name based on setting
        if (Settings::get('filter-sort-options-by-popularity', false)) {
            $results = $results->sortByDesc('result_count');
        } else {
            $results = $results->sortBy(fn ($entry) => strtolower((string) $entry->name));
        }

        $results = $results->values();

        return $this->enrichFilterResults($results, $filterName);
    }

    /**
     * Look up filter_index entries by slug or filter_id depending on facet attribute type.
     */
    protected function lookupFilterIndex(string $filterType, array $facetKeys, string $facetAttribute): Collection
    {
        $query = FilterIndex::query()
            ->where('filter_type', $filterType);

        if (str_ends_with($facetAttribute, '_id') || str_ends_with($facetAttribute, '_ids')) {
            $query->whereIn('filter_id', $facetKeys);
            return $query->get()->keyBy(fn ($entry) => (string)$entry->filter_id);
        }

        $query->whereIn('slug', $facetKeys);
        return $query->get()->keyBy('slug');
    }

    /**
     * Enrich filter results with reference table data for special filter types.
     */
    protected function enrichFilterResults(Collection $results, string $filterName): Collection
    {
        return match ($filterName) {
            'model' => $this->enrichModelResults($results),
            'colour' => $this->enrichColourResults($results),
            default => $results,
        };
    }

    /**
     * Add make_id and make relationship to model filter results.
     */
    protected function enrichModelResults(Collection $results): Collection
    {
        $slugs = $results->pluck('slug')->filter()->toArray();
        if (empty($slugs)) {
            return $results;
        }

        $selectedMakeSlugs = $this->selections['make'] ?? [];

        $query = VehicleModel::query()
            ->whereIn('slug', $slugs)
            ->with('make:id,slug,name');

        if (!empty($selectedMakeSlugs)) {
            $query->whereHas('make', fn ($q) => $q->whereIn('slug', $selectedMakeSlugs));
        }

        $models = $query->get()->keyBy('slug');

        return $results->map(function ($entry) use ($models) {
            $model = $models->get($entry->slug);
            if ($model) {
                $entry->make_id = $model->make_id;
                $entry->make = $model->make;
            }
            return $entry;
        });
    }

    /**
     * Add hex_code and gradient_hex_code to colour filter results.
     */
    protected function enrichColourResults(Collection $results): Collection
    {
        if (!Settings::get('filter-use-colour-mapping')) {
            return $results;
        }

        $names = $results->pluck('name')->filter()->toArray();
        if (empty($names)) {
            return $results;
        }

        $colours = Colour::query()
            ->whereIn('colour', $names)
            ->get()
            ->keyBy('colour');

        return $results->map(function ($entry) use ($colours) {
            $colour = $colours->get($entry->name);
            if ($colour) {
                $entry->hex_code = $colour->hex_code;
                $entry->gradient_hex_code = $colour->gradient_hex_code;
            }
            return $entry;
        });
    }

    /**
     * Get facet distribution for a specific filter, excluding that filter's selection.
     * This allows showing accurate counts for each filter option.
     */
    protected function getFacetDistributionForFilter(string $filterName): array
    {
        if (!isset($this->facetsByFilterType[$filterName])) {
            $facetAttribute = $this->facetAttributeMapping[$filterName] ?? null;

            if (!$facetAttribute) {
                return [];
            }

            // Build filters excluding the current filter type
            $filtersWithoutCurrent = $this->buildMeilisearchFiltersExcluding($filterName);
            $searchTerm = $this->selections['search'][0] ?? null;

            $this->facetsByFilterType[$filterName] = $this->meilisearch->getFacets(
                $filtersWithoutCurrent,
                [$facetAttribute],
                $searchTerm,
                $this->geoFilter
            );
        }

        return $this->facetsByFilterType[$filterName];
    }

    /**
     * Build Meilisearch filters excluding a specific filter type.
     */
    protected function buildMeilisearchFiltersExcluding(string $excludeFilterType): array
    {
        $filters = ['is_published' => true];

        if ($excludeFilterType !== 'is_sold') {
            $filters['is_sold'] = false;
        }

        $enabledFilters = $this->getEnabledFilterNames();

        foreach ($this->selections as $type => $values) {
            if (empty($values)) {
                continue;
            }

            // Skip the excluded filter type
            if ($type === $excludeFilterType) {
                continue;
            }

            // Handle postcode filter separately (uses geo search)
            if ($type === 'postcode') {
                continue;
            }

            if (!in_array($type, $enabledFilters, true)) {
                continue;
            }

            $filterMapping = $this->getMeilisearchFilterMapping($type, $values);
            if ($filterMapping !== null) {
                $filters = array_merge($filters, $filterMapping);
            }
        }

        // If both sold and available are selected (and is_sold isn't the excluded filter),
        // remove the is_sold filter to show all
        if ($excludeFilterType !== 'is_sold' && $this->isSoldFilterShowsAll()) {
            unset($filters['is_sold']);
        }

        return $filters;
    }

    /**
     * Get facet distribution from Meilisearch, cached for the current request.
     */
    protected function getFacetDistribution(): array
    {
        if ($this->cachedFacets === null) {
            $facetAttributes = array_values($this->facetAttributeMapping);
            $searchTerm = $this->selections['search'][0] ?? null;

            $this->cachedFacets = $this->meilisearch->getFacets(
                $this->meilisearchFilters,
                $facetAttributes,
                $searchTerm,
                $this->geoFilter
            );
        }

        return $this->cachedFacets;
    }

    protected function getResultCount(bool $include_extras = true): int
    {
        if ($this->cachedSearchResults !== null) {
            return $this->cachedSearchResults['total'];
        }

        $searchResults = $this->meilisearch->getVehicleIds(
            $this->meilisearchFilters,
            [],
            0,
            0,
            $this->selections['search'][0] ?? null,
            $this->geoFilter
        );

        return $searchResults['total'];
    }

    protected function getCheapestOffer(): ?float
    {
        $facetData = $this->getRangeFacetData();
        $stats = $facetData['stats']['price'] ?? null;

        return $stats ? (float)$stats['min'] : null;
    }

    public function getRepresentativeFinance(JsonResource $results): ?VehicleFinance
    {
        return parent::getRepresentativeFinance($results);
    }

    /**
     * Single Meilisearch call for ALL range filters.
     * Applies categorical constraints but excludes all range filter selections.
     */
    protected function getRangeFacetData(): array
    {
        if ($this->cachedRangeFacets === null) {
            $rangeFilterNames = array_keys($this->rangeFilterFacetConfig);
            $enabledFilters = $this->getEnabledFilterNames();
            $filters = ['is_published' => true, 'is_sold' => false];

            foreach ($this->selections as $type => $values) {
                if (empty($values) || in_array($type, $rangeFilterNames, true) || $type === 'postcode') {
                    continue;
                }

                if (!in_array($type, $enabledFilters, true)) {
                    continue;
                }

                $filterMapping = $this->getMeilisearchFilterMapping($type, $values);
                if ($filterMapping !== null) {
                    $filters = array_merge($filters, $filterMapping);
                }
            }

            if ($this->isSoldFilterShowsAll()) {
                unset($filters['is_sold']);
            }

            $attrs = collect($this->rangeFilterFacetConfig)
                ->map(fn ($c, $name) => $this->resolveRangeFilterAttr($name, $c))
                ->unique()
                ->values()
                ->toArray();

            $searchTerm = $this->selections['search'][0] ?? null;
            $this->cachedRangeFacets = $this->meilisearch->getFacetsWithStats(
                $filters,
                $attrs,
                $searchTerm,
                $this->geoFilter
            );
        }

        return $this->cachedRangeFacets;
    }

    /**
     * Resolve the Meilisearch attribute for a range filter.
     * Handles dynamic mileage attribute based on distance measurement setting.
     */
    protected function resolveRangeFilterAttr(string $filterName, array $config): string
    {
        if ($config['attr'] === null) {
            return Settings::get('automotive-distance_measurement') === 'mi' ? 'odometer_mi' : 'odometer_km';
        }

        return $config['attr'];
    }

    /**
     * Dispatch to config-based or discrete range handler based on filter config.
     */
    protected function buildRangeResultsFromFacets(string $filterName, IsFilter $filter): Collection
    {
        $config = $this->rangeFilterFacetConfig[$filterName];
        $attr = $this->resolveRangeFilterAttr($filterName, $config);
        $facetData = $this->getRangeFacetData();

        if ($config['type'] === 'config') {
            return $this->buildConfigRangeFromFacets($filter, $attr, $config, $facetData);
        }

        return $this->buildDiscreteRangeFromFacets($filter, $attr, $config, $facetData);
    }

    /**
     * Build config-based range filter results using facetStats min/max
     * to filter predefined config ranges.
     */
    protected function buildConfigRangeFromFacets(
        IsFilter $filter,
        string $attr,
        array $config,
        array $facetData
    ): Collection {
        $stats = $facetData['stats'][$attr] ?? null;
        if (!$stats) {
            return collect();
        }

        $min = floor($stats['min']);
        $max = ceil($stats['max']);
        $ranges = collect(Config::get($config['config_key']));

        // Filter by max: include up to max, plus one step over
        $filtered = $ranges->filter(
            fn ($range, $i) => $range < $max || ($i > 0 && $range > $max && $ranges[$i - 1] < $max)
        );

        // Filter by min: include above min, plus one step under
        $filtered = $filtered->filter(
            fn ($range, $i) => $range > $min
                || ($i < $ranges->count() - 1 && $range < $min && $ranges[$i + 1] > $min)
        );

        $result = $filtered->map(function ($range) use ($filter, $config) {
            $entry = ['id' => $range, 'name' => $filter->getSelectionName($range)];
            if (!empty($config['has_value'])) {
                $entry['value'] = $range;
            }
            return $entry;
        });

        if (!empty($config['slice_last'])) {
            $result = $result->slice(0, -1);
        }

        return $result->values();
    }

    /**
     * Build discrete range filter results using facetDistribution keys
     * as distinct values (for year, doors, seats).
     */
    protected function buildDiscreteRangeFromFacets(
        IsFilter $filter,
        string $attr,
        array $config,
        array $facetData
    ): Collection {
        $distribution = $facetData['distribution'][$attr] ?? [];
        if (empty($distribution)) {
            return collect();
        }

        $values = collect(array_keys($distribution))
            ->map(fn ($v) => (int)$v)
            ->when(
                isset($config['min_value']),
                fn ($c) => $c->filter(fn ($v) => $v >= $config['min_value'])
            );

        $values = ($config['sort'] ?? 'asc') === 'desc'
            ? $values->sortDesc()->values()
            : $values->sort()->values();

        return $values->map(fn ($value) => [
            'id' => $value,
            'name' => $filter->getSelectionName($value),
        ]);
    }
}
