<?php

namespace App\Http\Livewire;

use App\Helpers\SearchOptimizationHelper;
use App\Models\Locations\City;
use App\Overrides\Matchish\ScoutElasticSearch\ElasticSearch\EloquentHitsIteratorAggregate;
use App\Traits\ProfilePermissionTrait;
use Elastic\Elasticsearch\Client;
use Elastic\Elasticsearch\ClientBuilder;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
use Matchish\ScoutElasticSearch\MixedSearch;
use ONGR\ElasticsearchDSL\Highlight\Highlight;
use ONGR\ElasticsearchDSL\Query\Compound\BoolQuery;
use ONGR\ElasticsearchDSL\Query\FullText\MultiMatchQuery;
use ONGR\ElasticsearchDSL\Search;

class MainSearchBar extends Component
{
    use ProfilePermissionTrait;

    public $search;
    public $suggestions = [];
    public $fetchedResults = [];
    public $results = [];
    public int $total = 0;
    public $showResults = false;

    // Store the raw search term separately from display suggestions
    private $rawSearchTerm = '';

    // Map suggestions to their original search contexts
    private $suggestionMap = [];

    // Location-based search properties
    public $profileLocation = null;
    public $locationHierarchy = [];

    public function mount()
    {
        $this->loadProfileLocation();
    }

    public function render()
    {
        return view('livewire.main-search-bar');
    }

    public function selectSuggestion($suggestionIndex)
    {
        \Log::info('selectSuggestion called', ['index' => $suggestionIndex, 'suggestions' => $this->suggestions]);

        // Get the suggestion text from the suggestions collection
        $suggestion = $this->suggestions[$suggestionIndex] ?? null;

        \Log::info('Found suggestion', ['suggestion' => $suggestion]);

        if ($suggestion && isset($suggestion['text'])) {
            // Simply set the search to the suggestion text
            $this->search = $suggestion['text'];

            \Log::info('Set search to', ['search' => $this->search]);

            // Clear suggestions
            $this->suggestions = collect();

            // Execute search and redirect
            $this->updatedSearch();
            return $this->showSearchResults();
        }

        \Log::warning('selectSuggestion failed - no valid suggestion found');
    }

    private function loadProfileLocation()
    {
        try {
            if (function_exists('getActiveProfile')) {
                $activeProfile = getActiveProfile();
                if ($activeProfile && $activeProfile->locations) {
                    $this->profileLocation = $activeProfile->locations->first();

                    if ($this->profileLocation) {
                        $this->locationHierarchy = $this->profileLocation->getCompleteHierarchy();
                        
                    }
                }
            }
        } catch (\Exception $e) {
            \Log::warning('MainSearchBar: Failed to load profile location: ' . $e->getMessage());
            $this->profileLocation = null;
            $this->locationHierarchy = [];
        }
    }

    private function getLocationName($location)
    {
        if (!$location) {
            return null;
        }

        // If location is an array or collection, return null
        if (is_array($location) || ($location instanceof \Illuminate\Support\Collection)) {
            return null;
        }

        $locale = app()->getLocale();

        // Try to get translated name
        try {
            // Check if location has a 'name' property that is an array of translations
            if (isset($location->name) && is_array($location->name)) {
                // Find translation matching current locale
                foreach ($location->name as $translation) {
                    if (isset($translation['locale']) && $translation['locale'] === $locale && isset($translation['name'])) {
                        return $translation['name'];
                    }
                }
                // Fall back to English if current locale not found
                foreach ($location->name as $translation) {
                    if (isset($translation['locale']) && $translation['locale'] === 'en' && isset($translation['name'])) {
                        return $translation['name'];
                    }
                }
                // Fall back to first translation
                if (isset($location->name[0]['name'])) {
                    return $location->name[0]['name'];
                }
            }

            // Try to get from translations relationship
            if (method_exists($location, 'getRelation') && $location->relationLoaded('translations')) {
                $translations = $location->getRelation('translations');
                if ($translations && $translations->count() > 0) {
                    $translation = $translations->where('locale', $locale)->first();
                    if ($translation && isset($translation->name)) {
                        return $translation->name;
                    }
                }
            }
        } catch (\Exception $e) {
            \Log::warning('MainSearchBar: Failed to get translated location name', [
                'error' => $e->getMessage()
            ]);
        }

        // Fall back to default name property if it's a string
        if (isset($location->name) && is_string($location->name)) {
            return $location->name;
        }

        return null;
    }

    private function getProfileSearchFields(): array
    {
        $locale = app()->getLocale();

        // Text/tag fields — use per-field language analyzers (no override)
        return [
            'cyclos_skills^' . timebank_config('main_search_bar.boosted_fields.profile.cyclos_skills', '1'),
            'tags.contexts.tags.name_' . $locale . '^' . timebank_config('main_search_bar.boosted_fields.profile.name', '1'),
            'tags.contexts.categories.name_' . $locale . '^' . timebank_config('main_search_bar.boosted_fields.profile.tag_categories', '1'),
            'motivation_' . $locale . '^' . timebank_config('main_search_bar.boosted_fields.profile.motivation', '1'),
            'about_short_' . $locale . '^' . timebank_config('main_search_bar.boosted_fields.profile.about_short', '1'),
            'about_' . $locale . '^' . timebank_config('main_search_bar.boosted_fields.profile.about', '1'),
        ];
    }

    // Name fields use an edge n-gram analyzer at index time; standard analyzer must be used at query time.
    private function getProfileNameFields(): array
    {
        return [
            'name^' . timebank_config('main_search_bar.boosted_fields.profile.name', '1'),
            'full_name^' . timebank_config('main_search_bar.boosted_fields.profile.full_name', '1'),
        ];
    }

    // Location fields are searchable (e.g. "computer brussels" matches city) using standard analyzer.
    // Kept separate from proximity boosts in addLocationBoosts() which boost by the searcher's own location.
    private function getProfileLocationFields(): array
    {
        return [
            'locations.district^' . timebank_config('main_search_bar.boosted_fields.profile.district', '1'),
            'locations.city^' . timebank_config('main_search_bar.boosted_fields.profile.city', '1'),
            'locations.division^' . timebank_config('main_search_bar.boosted_fields.profile.division', '1'),
            'locations.country^' . timebank_config('main_search_bar.boosted_fields.profile.country', '1'),
        ];
    }

    // Cross-fields query covers name + location fields only.
    // This rewards profiles where different query terms match across name/location
    // (e.g. "computer" in tags AND "brussels" in city).
    // Free-text fields (about, cyclos_skills) are intentionally excluded to avoid
    // inflating scores for incidental word mentions in profile text.
    private function getProfileCrossFields(): array
    {
        return [
            'name',
            'full_name',
            'locations.city',
            'locations.district',
            'locations.division',
            'locations.country',
        ];
    }
    private function getPostSearchFields(): array
    {
        $locale = app()->getLocale();

        return [

            "post_translations.title_{$locale}^" . timebank_config('main_search_bar.boosted_fields.post.title', '3'),
            "post_translations.content_{$locale}^" . timebank_config('main_search_bar.boosted_fields.post.content', '1'),
            "post_translations.excerpt_{$locale}^" . timebank_config('main_search_bar.boosted_fields.post.excerpt', '2'), // Add excerpt
            "post_category.names.name_{$locale}^" . timebank_config('main_search_bar.boosted_fields.post.post_category_name', '2'),
            // 'locations.district^' . timebank_config('main_search_bar.boosted_fields.profile.district', '1'),
            // 'locations.city^' . timebank_config('main_search_bar.boosted_fields.profile.city', '1'),
            // 'locations.division^' . timebank_config('main_search_bar.boosted_fields.profile.division', '1'),
            // 'locations.country^' . timebank_config('main_search_bar.boosted_fields.profile.country', '1'),
        ];
    }

    private function getProfileHighlightFields(): array
    {
        $locale = app()->getLocale();
        
        // Apply global highlight settings to each field
        $highlightOptions = [
            'fragment_size' => timebank_config('main_search_bar.search.fragment_size', 80),
            'number_of_fragments' => timebank_config('main_search_bar.search.number_of_fragments', 1),
            'fragmenter' => timebank_config('main_search_bar.search.fragmenter', 'span'),
            'order' => timebank_config('main_search_bar.search.order', 'score'),
        ];

        return [
            // Content fields only - no location fields for highlighting
            'name' => $highlightOptions,
            'full_name' => $highlightOptions,
            'about_short_' . $locale => $highlightOptions,
            'about_' . $locale => $highlightOptions,
            'cyclos_skills' => $highlightOptions,
            'tags.contexts.tags.name_' . $locale => $highlightOptions,
            'tags.contexts.categories.name_' . $locale => $highlightOptions,
            // Location fields removed from highlighting (still used for search boosting and ordering)
        ];
    }

    private function getPostHighlightFields(): array
    {
        $locale = app()->getLocale();
        
        // Apply global highlight settings to each field
        $highlightOptions = [
            'fragment_size' => timebank_config('main_search_bar.search.fragment_size', 80),
            'number_of_fragments' => timebank_config('main_search_bar.search.number_of_fragments', 1),
            'fragmenter' => timebank_config('main_search_bar.search.fragmenter', 'span'),
            'order' => timebank_config('main_search_bar.search.order', 'score'),
        ];

        return [
            // Content fields only - no location fields for highlighting
            "post_translations.title_{$locale}" => $highlightOptions,
            "post_translations.content_{$locale}" => $highlightOptions,
            "post_translations.excerpt_{$locale}" => $highlightOptions,
            "post_category.names.name_{$locale}" => $highlightOptions,
            // Location fields removed from highlighting (still used for search boosting and ordering)
        ];
    }

    private function getCallHighlightFields(): array
    {
        $locale = app()->getLocale();

        $highlightOptions = [
            'fragment_size'      => timebank_config('main_search_bar.search.fragment_size', 80),
            'number_of_fragments' => timebank_config('main_search_bar.search.number_of_fragments', 1),
            'fragmenter'         => timebank_config('main_search_bar.search.fragmenter', 'span'),
            'order'              => timebank_config('main_search_bar.search.order', 'score'),
        ];

        $fields = [
            "tag.name_{$locale}"                  => $highlightOptions,
            "call_translations.content_{$locale}" => $highlightOptions,
        ];
        foreach (['en', 'nl', 'fr', 'de', 'es'] as $otherLocale) {
            if ($otherLocale !== $locale) {
                $fields["tag.name_{$otherLocale}"]                  = $highlightOptions;
                $fields["call_translations.content_{$otherLocale}"] = $highlightOptions;
            }
        }
        return $fields;
    }

    public function updatedSearch()
    {
        // Store the raw search term before any processing
        $this->rawSearchTerm = $this->search;

        // Process the search term
        $search = $this->search;
        // Allow Unicode letters (including German umlauts, Spanish accents, etc.), numbers, and spaces
        $search = preg_replace('/[^\p{L}\p{N}\s]/u', '', $search);
        $search = rtrim($search);

        if (strlen($search) < 2) {
            $this->suggestions = collect();
            $this->fetchedResults = [];
            $this->suggestionMap = [];
            return;
        }

        $cleanSearch = trim(str_replace('*', '', $search));

        try {
            // Use MixedSearch with proper highlighting
            $currentTime = now()->toISOString();
            $locale = app()->getLocale();

            // Create the main bool query
            $mainBoolQuery = new BoolQuery();

            // Profile queries use three separate sub-queries combined with minimum_should_match:1:
            // 1. Text/tag fields  — language-specific analyzers (education->educ stem etc.)
            // 2. Name fields      — standard analyzer (edge n-gram index, bypass n-gram at query time)
            // 3. Location fields  — standard analyzer (so "brussels" matches the city field)
            // A separate exact-name phrase SHOULD clause (^10) boosts profiles whose name matches exactly.
            // addLocationBoosts() adds proximity bonuses based on the searcher's own location.
            $exactNameFields = ['name^10', 'full_name^10'];
            $searchType = timebank_config('main_search_bar.search.type', 'best_fields');

            $userMatchBool = new BoolQuery();
            $userMatchBool->addParameter('minimum_should_match', 1);
            $userTextQuery = new MultiMatchQuery($this->getProfileSearchFields(), $cleanSearch);
            $userTextQuery->addParameter('type', $searchType);
            $userTextQuery->addParameter('boost', timebank_config('main_search_bar.boosted_models.user', 1));
            $userMatchBool->add($userTextQuery, BoolQuery::SHOULD);
            $userNameQuery = new MultiMatchQuery($this->getProfileNameFields(), $cleanSearch);
            $userNameQuery->addParameter('type', 'best_fields');
            $userNameQuery->addParameter('analyzer', 'standard');
            $userMatchBool->add($userNameQuery, BoolQuery::SHOULD);
            $userLocationQuery = new MultiMatchQuery($this->getProfileLocationFields(), $cleanSearch);
            $userLocationQuery->addParameter('type', 'best_fields');
            $userLocationQuery->addParameter('analyzer', 'standard');
            $userLocationQuery->addParameter('boost', 3.0);
            $userMatchBool->add($userLocationQuery, BoolQuery::SHOULD);
            $userCrossQuery = new MultiMatchQuery($this->getProfileCrossFields(), $cleanSearch);
            $userCrossQuery->addParameter('type', 'cross_fields');
            $userCrossQuery->addParameter('analyzer', 'standard');
            $userCrossQuery->addParameter('boost', 5.0);
            $userMatchBool->add($userCrossQuery, BoolQuery::SHOULD);
            $userBoolQuery = new BoolQuery();
            $userBoolQuery->add(new \ONGR\ElasticsearchDSL\Query\TermLevel\TermQuery('__class_name', 'App\Models\User'), BoolQuery::MUST);
            $userBoolQuery->add($userMatchBool, BoolQuery::MUST);
            $userExactNameQuery = new MultiMatchQuery($exactNameFields, $cleanSearch);
            $userExactNameQuery->addParameter('type', 'phrase');
            $userExactNameQuery->addParameter('analyzer', 'standard');
            $userBoolQuery->add($userExactNameQuery, BoolQuery::SHOULD);
            $this->addLocationBoosts($userBoolQuery);

            $orgMatchBool = new BoolQuery();
            $orgMatchBool->addParameter('minimum_should_match', 1);
            $orgTextQuery = new MultiMatchQuery($this->getProfileSearchFields(), $cleanSearch);
            $orgTextQuery->addParameter('type', $searchType);
            $orgTextQuery->addParameter('boost', timebank_config('main_search_bar.boosted_models.organization', 3));
            $orgMatchBool->add($orgTextQuery, BoolQuery::SHOULD);
            $orgNameQuery = new MultiMatchQuery($this->getProfileNameFields(), $cleanSearch);
            $orgNameQuery->addParameter('type', 'best_fields');
            $orgNameQuery->addParameter('analyzer', 'standard');
            $orgMatchBool->add($orgNameQuery, BoolQuery::SHOULD);
            $orgLocationQuery = new MultiMatchQuery($this->getProfileLocationFields(), $cleanSearch);
            $orgLocationQuery->addParameter('type', 'best_fields');
            $orgLocationQuery->addParameter('analyzer', 'standard');
            $orgLocationQuery->addParameter('boost', 3.0);
            $orgMatchBool->add($orgLocationQuery, BoolQuery::SHOULD);
            $orgCrossQuery = new MultiMatchQuery($this->getProfileCrossFields(), $cleanSearch);
            $orgCrossQuery->addParameter('type', 'cross_fields');
            $orgCrossQuery->addParameter('analyzer', 'standard');
            $orgCrossQuery->addParameter('boost', 5.0);
            $orgMatchBool->add($orgCrossQuery, BoolQuery::SHOULD);
            $orgBoolQuery = new BoolQuery();
            $orgBoolQuery->add(new \ONGR\ElasticsearchDSL\Query\TermLevel\TermQuery('__class_name', 'App\Models\Organization'), BoolQuery::MUST);
            $orgBoolQuery->add($orgMatchBool, BoolQuery::MUST);
            $orgExactNameQuery = new MultiMatchQuery($exactNameFields, $cleanSearch);
            $orgExactNameQuery->addParameter('type', 'phrase');
            $orgExactNameQuery->addParameter('analyzer', 'standard');
            $orgBoolQuery->add($orgExactNameQuery, BoolQuery::SHOULD);
            $this->addLocationBoosts($orgBoolQuery);

            $bankMatchBool = new BoolQuery();
            $bankMatchBool->addParameter('minimum_should_match', 1);
            $bankTextQuery = new MultiMatchQuery($this->getProfileSearchFields(), $cleanSearch);
            $bankTextQuery->addParameter('type', $searchType);
            $bankTextQuery->addParameter('boost', timebank_config('main_search_bar.boosted_models.bank', 3));
            $bankMatchBool->add($bankTextQuery, BoolQuery::SHOULD);
            $bankNameQuery = new MultiMatchQuery($this->getProfileNameFields(), $cleanSearch);
            $bankNameQuery->addParameter('type', 'best_fields');
            $bankNameQuery->addParameter('analyzer', 'standard');
            $bankMatchBool->add($bankNameQuery, BoolQuery::SHOULD);
            $bankLocationQuery = new MultiMatchQuery($this->getProfileLocationFields(), $cleanSearch);
            $bankLocationQuery->addParameter('type', 'best_fields');
            $bankLocationQuery->addParameter('analyzer', 'standard');
            $bankLocationQuery->addParameter('boost', 3.0);
            $bankMatchBool->add($bankLocationQuery, BoolQuery::SHOULD);
            $bankCrossQuery = new MultiMatchQuery($this->getProfileCrossFields(), $cleanSearch);
            $bankCrossQuery->addParameter('type', 'cross_fields');
            $bankCrossQuery->addParameter('analyzer', 'standard');
            $bankCrossQuery->addParameter('boost', 5.0);
            $bankMatchBool->add($bankCrossQuery, BoolQuery::SHOULD);
            $bankBoolQuery = new BoolQuery();
            $bankBoolQuery->add(new \ONGR\ElasticsearchDSL\Query\TermLevel\TermQuery('__class_name', 'App\Models\Bank'), BoolQuery::MUST);
            $bankBoolQuery->add($bankMatchBool, BoolQuery::MUST);
            $bankExactNameQuery = new MultiMatchQuery($exactNameFields, $cleanSearch);
            $bankExactNameQuery->addParameter('type', 'phrase');
            $bankExactNameQuery->addParameter('analyzer', 'standard');
            $bankBoolQuery->add($bankExactNameQuery, BoolQuery::SHOULD);
            $this->addLocationBoosts($bankBoolQuery);

            // Add profile queries to main query with their individual boosts
            $mainBoolQuery->add($userBoolQuery, BoolQuery::SHOULD);
            $mainBoolQuery->add($orgBoolQuery, BoolQuery::SHOULD);
            $mainBoolQuery->add($bankBoolQuery, BoolQuery::SHOULD);

            // Add posts search query
            $postsBoolQuery = $this->addPostsSearch($cleanSearch, $currentTime, $locale);
            $mainBoolQuery->add($postsBoolQuery, BoolQuery::SHOULD);

            // Add calls search query
            $callsBoolQuery = $this->addCallsSearch($cleanSearch, $currentTime, $locale);
            $mainBoolQuery->add($callsBoolQuery, BoolQuery::SHOULD);

            // Create highlight configuration
            $highlight = new Highlight();
            $highlight->addParameter('fragment_size', timebank_config('main_search_bar.search.fragment_size', 80));
            $highlight->addParameter('number_of_fragments', timebank_config('main_search_bar.search.number_of_fragments', 1));
            $highlight->addParameter('fragmenter', timebank_config('main_search_bar.search.fragmenter', 'span'));
            $highlight->addParameter('order', timebank_config('main_search_bar.search.order', 'score'));
            
            // Limit total fragments across all fields - use max_analyzed_offset to limit highlighting
            $highlight->addParameter('max_analyzed_offset', timebank_config('main_search_bar.search.fragment_size', 80));
            $highlight->addParameter('require_field_match', true);

            // Add pre/post tags for highlighting
            $preTags = timebank_config('main_search_bar.search.pre-tags');
            $postTags = timebank_config('main_search_bar.search.post-tags');
            if ($preTags && $postTags) {
                $highlight->addParameter('pre_tags', is_array($preTags) ? $preTags : [$preTags]);
                $highlight->addParameter('post_tags', is_array($postTags) ? $postTags : [$postTags]);
            }

            // Add profile highlight fields
            foreach ($this->getProfileHighlightFields() as $field => $options) {
                $highlight->addField($field, $options);
            }

            // Add post highlight fields
            foreach ($this->getPostHighlightFields() as $field => $options) {
                $highlight->addField($field, $options);
            }

            // Add call highlight fields
            foreach ($this->getCallHighlightFields() as $field => $options) {
                $highlight->addField($field, $options);
            }

            // Execute MixedSearch with callback
            // Pass empty string so SearchFactory does not inject a query_string MUST clause —
            // all matching is handled by our own $mainBoolQuery in the callback.
            $rawResponse = MixedSearch::search('', function (Client $client, Search $body) use ($mainBoolQuery, $highlight, $cleanSearch, $locale) {
                // Add the query
                $body->addQuery($mainBoolQuery);

                // Add highlighting
                $body->addHighlight($highlight);

                // Set size
                $body->setSize(timebank_config('main_search_bar.search.max_results', 50));

                // Log the query for debugging
                $queryArray = $body->toArray();
                \Log::info('MainSearchBar: Elasticsearch query', [
                    'locale' => $locale,
                    'search_term' => $cleanSearch,
                    'query' => json_encode($queryArray)
                ]);

                // Execute the search
                return $client->search([
                    'index' => implode(',', timebank_config('main_search_bar.model_indices', [])),
                    'body' => $queryArray,
                ])->asArray();
            })->raw();

            // Get models from the response
            $results = collect();
            if (isset($rawResponse['hits']['hits'])) {
                foreach ($rawResponse['hits']['hits'] as $hit) {
                    $modelClass = $hit['_source']['__class_name'] ?? null;
                    $modelId = $hit['_source']['id'] ?? null;
                    
                    if ($modelClass && $modelId && class_exists($modelClass)) {
                        try {
                            $model = $modelClass::find($modelId);
                            if ($model) {
                                $results->push($model);
                            }
                        } catch (\Exception $e) {
                            \Log::warning("MainSearchBar: Could not load model {$modelClass}:{$modelId}", ['error' => $e->getMessage()]);
                        }
                    }
                }
            }

            $this->processMixedResults($results, $rawResponse, $cleanSearch);
        } catch (\Exception $e) {
            \Log::error('MainSearchBar: Enhanced search failed', [
                'error' => $e->getMessage(),
                'search_term' => $cleanSearch
            ]);

            $this->suggestions = collect();
            $this->fetchedResults = [];
            $this->total = 0;
            $this->suggestionMap = [];
        }
    }


    private function addPostsSearch(string $cleanSearch, string $currentTime, string $locale): BoolQuery
    {
        $postsBoolQuery = new BoolQuery();

        $postsBoolQuery->add(
            new \ONGR\ElasticsearchDSL\Query\TermLevel\TermQuery('__class_name', 'App\Models\Post'),
            BoolQuery::MUST
        );

        $postSearchFields = [
            'post_translations.title_' . $locale . '^' .timebank_config('main_search_bar.boosted_fields.post.title', '1'),
            'post_translations.content_' . $locale . '^' . timebank_config('main_search_bar.boosted_fields.post.content', '1'),
            'post_translations.excerpt_' . $locale . '^' . timebank_config('main_search_bar.boosted_fields.post.excerpt', '1'),
            'post_translations.category.names.name_' . $locale . '^' . timebank_config('main_search_bar.boosted_fields.post.category_name', '1'),
        ];

        $postMultiMatchQuery = new MultiMatchQuery($postSearchFields, $cleanSearch);
        $postMultiMatchQuery->addParameter('boost', timebank_config('main_search_bar.boosted_models.post', 4));
        $postsBoolQuery->add($postMultiMatchQuery, BoolQuery::MUST);

        $this->addPostPublicationFilters($postsBoolQuery, $currentTime, $locale);

        $includedCategoryIds = timebank_config('main_search_bar.category_ids_posts');
        if (!empty($includedCategoryIds)) {
            $categoryBoolQuery = new BoolQuery();
            foreach ($includedCategoryIds as $categoryId) {
                $categoryBoolQuery->add(
                    new \ONGR\ElasticsearchDSL\Query\TermLevel\TermQuery('category_id', $categoryId),
                    BoolQuery::SHOULD
                );
            }
            $postsBoolQuery->add($categoryBoolQuery, BoolQuery::MUST);
        }

        return $postsBoolQuery;
    }

    private function addPostPublicationFilters(BoolQuery $boolQuery, string $currentTime, string $locale): void
    {
        // From date: must exist AND be in the past (null means NOT published)
        $boolQuery->add(
            new \ONGR\ElasticsearchDSL\Query\TermLevel\ExistsQuery(
                "post_translations.from_{$locale}"
            ),
            BoolQuery::MUST
        );
        $boolQuery->add(
            new \ONGR\ElasticsearchDSL\Query\TermLevel\RangeQuery(
                "post_translations.from_{$locale}",
                ['lte' => $currentTime]
            ),
            BoolQuery::MUST
        );

        // Till date: (NOT exists) OR (exists AND in future) = never expires OR not yet expired
        $tillFilter = new BoolQuery();
        // Option 1: field doesn't exist (null)
        $tillNotExists = new BoolQuery();
        $tillNotExists->add(
            new \ONGR\ElasticsearchDSL\Query\TermLevel\ExistsQuery(
                "post_translations.till_{$locale}"
            ),
            BoolQuery::MUST_NOT
        );
        $tillFilter->add($tillNotExists, BoolQuery::SHOULD);

        // Option 2: field exists and is in the future
        $tillInFuture = new BoolQuery();
        $tillInFuture->add(
            new \ONGR\ElasticsearchDSL\Query\TermLevel\ExistsQuery(
                "post_translations.till_{$locale}"
            ),
            BoolQuery::MUST
        );
        $tillInFuture->add(
            new \ONGR\ElasticsearchDSL\Query\TermLevel\RangeQuery(
                "post_translations.till_{$locale}",
                ['gte' => $currentTime]
            ),
            BoolQuery::MUST
        );
        $tillFilter->add($tillInFuture, BoolQuery::SHOULD);
        $boolQuery->add($tillFilter, BoolQuery::MUST);

        // Deleted date: (NOT exists) OR (exists AND in future) = not deleted OR scheduled for future
        $deletionFilter = new BoolQuery();
        // Option 1: field doesn't exist (null)
        $deletionNotExists = new BoolQuery();
        $deletionNotExists->add(
            new \ONGR\ElasticsearchDSL\Query\TermLevel\ExistsQuery(
                "post_translations.deleted_at_{$locale}"
            ),
            BoolQuery::MUST_NOT
        );
        $deletionFilter->add($deletionNotExists, BoolQuery::SHOULD);

        // Option 2: field exists and is in the future
        $deletionInFuture = new BoolQuery();
        $deletionInFuture->add(
            new \ONGR\ElasticsearchDSL\Query\TermLevel\ExistsQuery(
                "post_translations.deleted_at_{$locale}"
            ),
            BoolQuery::MUST
        );
        $deletionInFuture->add(
            new \ONGR\ElasticsearchDSL\Query\TermLevel\RangeQuery(
                "post_translations.deleted_at_{$locale}",
                ['gt' => $currentTime]
            ),
            BoolQuery::MUST
        );
        $deletionFilter->add($deletionInFuture, BoolQuery::SHOULD);
        $boolQuery->add($deletionFilter, BoolQuery::MUST);
    }


    private function addCallsSearch(string $cleanSearch, string $currentTime, string $locale): BoolQuery
    {
        $callsBoolQuery = new BoolQuery();

        $callsBoolQuery->add(
            new \ONGR\ElasticsearchDSL\Query\TermLevel\TermQuery('__class_name', 'App\Models\Call'),
            BoolQuery::MUST
        );

        // Search current locale with higher boost, plus all other locales as fallback
        // This handles compound words in NL/DE where a search term may be part of a compound
        $callSearchFields = [
            'tag.name_' . $locale . '^3',
            'call_translations.content_' . $locale . '^1',
            'callable.name^1',
        ];
        foreach (['en', 'nl', 'fr', 'de', 'es'] as $otherLocale) {
            if ($otherLocale !== $locale) {
                $callSearchFields[] = 'tag.name_' . $otherLocale . '^1';
                $callSearchFields[] = 'call_translations.content_' . $otherLocale . '^0.5';
            }
        }

        // Standard multi-match across all locale fields
        $callMultiMatchQuery = new \ONGR\ElasticsearchDSL\Query\FullText\MultiMatchQuery($callSearchFields, $cleanSearch);
        $callMultiMatchQuery->addParameter('boost', timebank_config('main_search_bar.boosted_models.post', 4));
        $callsBoolQuery->add($callMultiMatchQuery, BoolQuery::SHOULD);

        // Wildcard fallback for compound words (e.g. NL/DE where search term is part of a compound)
        // Added as SHOULD alongside multi-match; the outer MUST requirement is satisfied if either matches
        foreach (['en', 'nl', 'fr', 'de', 'es'] as $wLocale) {
            $callsBoolQuery->add(
                new \ONGR\ElasticsearchDSL\Query\TermLevel\WildcardQuery('tag.name_' . $wLocale, strtolower($cleanSearch) . '*'),
                BoolQuery::SHOULD
            );
        }

        // Require at least one text clause to match (minimum_should_match on the should clauses)
        $callsBoolQuery->addParameter('minimum_should_match', 1);

        // from: must exist and be in the past
        $callsBoolQuery->add(
            new \ONGR\ElasticsearchDSL\Query\TermLevel\ExistsQuery('from'),
            BoolQuery::MUST
        );
        $callsBoolQuery->add(
            new \ONGR\ElasticsearchDSL\Query\TermLevel\RangeQuery('from', ['lte' => $currentTime]),
            BoolQuery::MUST
        );

        // till: not exists (no expiry) OR exists and in the future
        $tillFilter = new BoolQuery();
        $tillNotExists = new BoolQuery();
        $tillNotExists->add(new \ONGR\ElasticsearchDSL\Query\TermLevel\ExistsQuery('till'), BoolQuery::MUST_NOT);
        $tillFilter->add($tillNotExists, BoolQuery::SHOULD);
        $tillInFuture = new BoolQuery();
        $tillInFuture->add(new \ONGR\ElasticsearchDSL\Query\TermLevel\ExistsQuery('till'), BoolQuery::MUST);
        $tillInFuture->add(new \ONGR\ElasticsearchDSL\Query\TermLevel\RangeQuery('till', ['gte' => $currentTime]), BoolQuery::MUST);
        $tillFilter->add($tillInFuture, BoolQuery::SHOULD);
        $callsBoolQuery->add($tillFilter, BoolQuery::MUST);

        return $callsBoolQuery;
    }


    private function addLocationBoosts(BoolQuery $boolQuery)
    {
        if (empty($this->locationHierarchy)) {
            return;
        }

        if (isset($this->locationHierarchy['district']) && $this->locationHierarchy['district']) {
            $districtName = $this->getLocationName($this->locationHierarchy['district']);
            if ($districtName) {
                $districtQuery = new \ONGR\ElasticsearchDSL\Query\FullText\MatchQuery('locations.district', $districtName);
                $districtQuery->addParameter('boost', timebank_config('search_optimization.location_boosts.same_district', 5.0));
                $boolQuery->add($districtQuery, BoolQuery::SHOULD);
            }
        }

        if (isset($this->locationHierarchy['city']) && $this->locationHierarchy['city']) {
            $cityName = $this->getLocationName($this->locationHierarchy['city']);
            if ($cityName) {
                $cityQuery = new \ONGR\ElasticsearchDSL\Query\FullText\MatchQuery('locations.city', $cityName);
                $cityQuery->addParameter('boost', timebank_config('search_optimization.location_boosts.same_city', 3.0));
                $boolQuery->add($cityQuery, BoolQuery::SHOULD);
            }
        }

        if (isset($this->locationHierarchy['division']) && $this->locationHierarchy['division']) {
            $divisionName = $this->getLocationName($this->locationHierarchy['division']);
            \Log::info('MainSearchBar: Division location boost', [
                'division_object' => json_encode($this->locationHierarchy['division']),
                'division_name' => $divisionName,
                'locale' => app()->getLocale()
            ]);
            if ($divisionName) {
                $divisionQuery = new \ONGR\ElasticsearchDSL\Query\FullText\MatchQuery('locations.division', $divisionName);
                $divisionQuery->addParameter('boost', timebank_config('search_optimization.location_boosts.same_division', 2.0));
                $boolQuery->add($divisionQuery, BoolQuery::SHOULD);
            }
        }

        if (isset($this->locationHierarchy['country']) && $this->locationHierarchy['country']) {
            $countryName = $this->getLocationName($this->locationHierarchy['country']);
            if ($countryName) {
                $countryQuery = new \ONGR\ElasticsearchDSL\Query\FullText\MatchQuery('locations.country', $countryName);
                $countryQuery->addParameter('boost', timebank_config('search_optimization.location_boosts.same_country', 1.5));
                $boolQuery->add($countryQuery, BoolQuery::SHOULD);
            }
        }
    }

    private function processMixedResults($models, array $rawResponse, string $cleanSearch): void
    {
        $cardData = [];

        foreach ($models as $model) {
            $type = get_class($model);

            if ($type === \App\Models\Post::class) {
                $result = $this->processPostCard($model);
            } elseif ($type === \App\Models\Call::class) {
                // Always check current DB state regardless of index freshness
                if ($model->trashed()) continue;
                if ($model->is_suppressed) continue;
                if ($model->is_paused) continue;
                if ($model->till !== null && $model->till->isPast()) continue;
                if (!$model->is_public && !\Illuminate\Support\Facades\Auth::check()) continue;
                $result = $this->processCallCard($model);
            } else {
                $result = $this->processProfileCard($model);
            }

            if (!empty($result)) {
                $cardData[] = $result;
            }
        }

        $results = [];
        $rawHits = $rawResponse['hits']['hits'] ?? [];
        // Key by model+id to avoid collisions between different model types with the same id
        $cardDataByKey = collect($cardData)->keyBy(fn($r) => $r['model'] . ':' . $r['id']);

        foreach ($rawHits as $rawItem) {
            $id        = $rawItem['_source']['id'] ?? null;
            $modelClass = $rawItem['_source']['__class_name'] ?? null;
            $key = $modelClass . ':' . $id;
            if ($id && isset($cardDataByKey[$key])) {
                $result = $cardDataByKey[$key];
                $result['score'] = round(($rawItem['_score'] ?? 1.0) / 4, 1);

                // Limit highlights to only the most important one per result
                $highlight = $rawItem['highlight'] ?? [];
                $limitedHighlight = $this->limitHighlights($highlight);

                // CRITICAL XSS PROTECTION POINT
                // Sanitize highlights to prevent XSS attacks from user-generated content
                // Elasticsearch returns highlights containing user profile data that could include malicious HTML/JS
                // This MUST be sanitized before caching or displaying to prevent stored XSS attacks
                // DO NOT remove or bypass this sanitization - it protects all search result displays
                $result['highlight'] = $this->sanitizeHighlights($limitedHighlight);

                $results[] = $result;
            }
        }

        $extractedData = $this->processAndScoreResults($results, $cleanSearch);

        if (timebank_config('main_search_bar.suggestions') > 0) {
            // Create suggestions with mapped search terms
            $this->suggestionMap = [];
            $suggestionIndex = 0;

            $suggestions = collect($extractedData)->flatMap(function ($item) use (&$suggestionIndex, $cleanSearch) {
                $itemSuggestions = $item['suggest'] ?? [];
                $mappedSuggestions = [];

                foreach ($itemSuggestions as $suggestion) {
                    $this->suggestionMap[$suggestionIndex] = [
                        'display_text' => $suggestion,
                        'original_term' => $cleanSearch, // Always use the original search term
                        'field_source' => $item['highlight_field'] ?? 'unknown'
                    ];
                    $mappedSuggestions[] = [
                        'text' => $suggestion,
                        'index' => $suggestionIndex
                    ];
                    $suggestionIndex++;
                }

                return $mappedSuggestions;
            })->unique('text')->take(timebank_config('main_search_bar.suggestions'));

            $this->suggestions = $suggestions;
        }

        $this->fetchedResults = $extractedData->values()->all();
        $this->total = $rawResponse['hits']['total']['value'] ?? $models->count();
    }

    private function processPostCard($model): array
    {
        // Load relationships only when needed
        if (!$model->relationLoaded('category')) {
            $model = \App\Models\Post::with(['category.categoryable'])->find($model->id);
        }

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

        $locale = app()->getLocale();
        $translation = $model->translations()->where('locale', $locale)->first();

        // Handle missing translation
        if (!$translation) {
            return []; // Skip this post
        }

        // Check publication status using dates (same logic as in search)
        $currentTime = now();
        $isPublished = true;
        $filterReason = '';

        // From date MUST exist and be in the past (null = NOT published)
        if (!$translation->from) {
            $isPublished = false; // No publication date = not published
            $filterReason = 'No from date (unpublished)';
        } elseif ($currentTime->lt($translation->from)) {
            $isPublished = false; // Not yet published
            $filterReason = 'Future from date: ' . $translation->from;
        }

        // Till date can be null (never expires) or must be in the future
        if ($translation->till && $currentTime->gt($translation->till)) {
            $isPublished = false; // Publication ended
            $filterReason = 'Past till date: ' . $translation->till;
        }

        // Deleted date can be null (not deleted) or must be in the future
        if ($translation->deleted_at && $currentTime->gte($translation->deleted_at)) {
            $isPublished = false; // Scheduled deletion occurred
            $filterReason = 'Deleted at: ' . $translation->deleted_at;
        }

        if (!$isPublished) {
            return []; // Skip unpublished posts
        }

        $categoryId = optional($model->category)->id;
        $photoUrl = $model->getFirstMediaUrl('posts', 'hero');
        $categoryName = null;

        if ($model->category && $model->category->relationLoaded('translations')) {
            $categoryName = optional($model->category->translations->where('locale', $locale)->first())->name;
        }

        return [
            'id' => $model->id,
            'model' => get_class($model),
            'model_object' => $model,
            'category_id' => $categoryId,
            'category' => $categoryName,
            'title' => $translation->title ?? '',
            'subtitle' => $translation->excerpt ?? '',
            'photo' => $photoUrl,
            'location_short' => '',
            'location' => '',
            'status' => '',
            'location_proximity' => $this->calculateLocationProximity($model),
        ];
    }

    private function processCallCard($model): array
    {
        $model->loadMissing([
            'callable',
            'tag.contexts.category.translations',
            'tag.contexts.category.ancestors.translations',
            'location.city.translations',
            'location.country.translations',
        ]);

        $tag         = $model->tag;
        $tagName     = $tag?->translation?->name ?? $tag?->name;
        $tagCategory = $tag?->contexts->first()?->category;
        $tagColor    = $tagCategory?->relatedColor ?? 'gray';
        $city        = optional($model->location?->city?->translations->first())->name;
        $country     = optional($model->location?->country?->translations->first())->name;

        $tagCategories = [];
        if ($tagCategory) {
            $ancestors = $tagCategory->ancestorsAndSelf()->get()->reverse();
            $locale = app()->getLocale();
            foreach ($ancestors as $cat) {
                $catName = $cat->translations->firstWhere('locale', $locale)?->name
                    ?? $cat->translations->first()?->name
                    ?? '';
                if ($catName) {
                    $tagCategories[] = [
                        'name'  => $catName,
                        'color' => $cat->relatedColor ?? 'gray',
                    ];
                }
            }
        }

        return [
            'id'                 => $model->id,
            'model'              => get_class($model),
            'model_object'       => $model,
            'category_id'        => null,
            'category'           => trans_with_platform('@PLATFORM_NAME@ call'),
            'title'              => $tagName ?? '',
            'subtitle'           => '',
            'photo'              => $model->callable?->profile_photo_url ?? '',
            'location_short'     => $city ?? '',
            'location'           => collect([$city, $country])->filter()->implode(', '),
            'tag_name'           => $tagName,
            'tag_color'          => $tagColor,
            'tag_categories'     => $tagCategories,
            'callable_name'      => $model->callable?->name ?? '',
            'till'               => $model->till,
            'status'             => '',
            'location_proximity' => $this->calculateLocationProximity($model),
        ];
    }

    private function processProfileCard($model): array
    {
        $canManageProfiles = $this->getCanManageProfiles();
        if (!$canManageProfiles) {
            if (
                timebank_config('profile_inactive.profile_search_hidden')
                && !empty($model->inactive_at)
                && \Carbon\Carbon::parse($model->inactive_at)->isPast()
            ) {
                return [];
            }

            if (
                timebank_config('profile_email_unverified.profile_search_hidden')
                && empty($model->email_verified_at)
            ) {
                return [];
            }

            if (
                !empty($model->deleted_at)
                && \Carbon\Carbon::parse($model->deleted_at)->isPast()
            ) {
                return [];
            }

            if (
                timebank_config('profile_incomplete.profile_search_hidden')
                && method_exists($model, 'hasIncompleteProfile')
                && $model->hasIncompleteProfile($model)
            ) {
                return [];
            }
        }

        // Load locations only when needed
        if (!$model->relationLoaded('locations')) {
            $locations = $model->locations()->with([
                'district.translations',
                'city.translations',
                'division.translations',
                'country'
            ])->get();
        } else {
            $locations = $model->locations;
        }

        $firstLocation = $locations->first();
        $locationShort = null;
        $location = '';

        if ($firstLocation) {
            $city = null;
            $district = null;
            $division = null;
            $country = null;

            if ($firstLocation->city) {
                $cityTranslation = $firstLocation->city->translations->where('locale', app()->getLocale())->first();
                $city = $cityTranslation ? $cityTranslation->name : $firstLocation->city->name;
            }
            if ($firstLocation->district) {
                $districtTranslation = $firstLocation->district->translations->where('locale', app()->getLocale())->first();
                $district = $districtTranslation ? $districtTranslation->name : $firstLocation->district->name;
            }
            if ($firstLocation->division) {
                $divisionTranslation = $firstLocation->division->translations->where('locale', app()->getLocale())->first();
                $division = $divisionTranslation ? $divisionTranslation->name : $firstLocation->division->name;
            }
            if (!$firstLocation->division && $city != null) {
                $cityId = $firstLocation->city->id;
                $cityModel = City::find($cityId);
                if ($cityModel && $cityModel->division) {
                    $divisionTranslation = $cityModel->division->translations->where('locale', app()->getLocale())->first();
                    $division = $divisionTranslation ? $divisionTranslation->name : $cityModel->division->name;
                }
            }
            if ($firstLocation->country) {
                $country = $firstLocation->country->code;
            }

            if ($city) {
                $locationShort = $city . ', ' . $division;
                if ($district) {
                    $location = $city . ' ' . $district . ', ' . $division . ', ' . $country;
                } else {
                    $location = $city . ', ' . $division . ', ' . $country;
                }
            } else {
                if ($country && !$city) {
                    $division ? $locationShort = $division . ', ' . $country : $locationShort = $country;
                    $division ? $location = $division . ', ' . $country : $location = $country;
                }
            }
        }

        $status = '';

        return [
            'id' => $model->id,
            'model' => get_class($model),
            'model_object' => $model,
            'name' => $model->name ?? '',
            'photo' => $model->profile_photo_url ?? '',
            'location_short' => $locationShort ?? '',
            'location' => $location,
            'status' => $status,
            'type' => strtolower(class_basename($model)),
            'location_proximity' => $this->calculateLocationProximity($model),
        ];
    }

    private function calculateLocationProximity($model): array
    {
        if (empty($this->locationHierarchy)) {
            return ['level' => 'unknown', 'distance' => null];
        }

        $modelLocation = null;

        if (get_class($model) === \App\Models\Call::class) {
            $modelLocation = $model->location;
            if (!$modelLocation) {
                return ['level' => 'no_location', 'distance' => null];
            }
            try {
                $modelHierarchy = $modelLocation->getCompleteHierarchy();
                return $this->compareLocationHierarchies($modelHierarchy);
            } catch (\Exception $e) {
                return ['level' => 'error', 'distance' => null];
            }
        }

        if (get_class($model) === 'App\Models\Post') {
            if ($model->category && $model->category->categoryable) {
                $categoryable = $model->category->categoryable;

                if (in_array(get_class($categoryable), [
                    'App\Models\Locations\City',
                    'App\Models\Locations\District',
                    'App\Models\Locations\Division',
                    'App\Models\Locations\Country'
                ])) {
                    try {
                        $modelHierarchy = $this->buildLocationHierarchyFromCategoryable($categoryable);
                        return $this->compareLocationHierarchies($modelHierarchy);
                    } catch (\Exception $e) {
                        return ['level' => 'error', 'distance' => null];
                    }
                }
            }
            return ['level' => 'no_location', 'distance' => null];
        } else {
            $modelLocation = $model->locations->first();
        }

        if (!$modelLocation) {
            return ['level' => 'no_location', 'distance' => null];
        }

        try {
            $modelHierarchy = $modelLocation->getCompleteHierarchy();
            return $this->compareLocationHierarchies($modelHierarchy);
        } catch (\Exception $e) {
            return ['level' => 'error', 'distance' => null];
        }
    }

    private function buildLocationHierarchyFromCategoryable($categoryable): array
    {
        $hierarchy = [
            'country' => null,
            'division' => null,
            'city' => null,
            'district' => null,
        ];

        $categoryableClass = get_class($categoryable);

        switch ($categoryableClass) {
            case 'App\Models\Locations\Country':
                $hierarchy['country'] = $categoryable;
                break;

            case 'App\Models\Locations\Division':
                $hierarchy['division'] = $categoryable;
                $hierarchy['country'] = $categoryable->country ?? null;
                break;

            case 'App\Models\Locations\City':
                $hierarchy['city'] = $categoryable;
                $hierarchy['division'] = $categoryable->division ?? null;
                $hierarchy['country'] = $categoryable->country ?? null;
                break;

            case 'App\Models\Locations\District':
                $hierarchy['district'] = $categoryable;
                $hierarchy['city'] = $categoryable->city ?? null;
                $hierarchy['division'] = $categoryable->city->division ?? null;
                $hierarchy['country'] = $categoryable->city->country ?? null;
                break;
        }

        return $hierarchy;
    }

    private function compareLocationHierarchies($modelHierarchy): array
    {
        if (
            isset($this->locationHierarchy['district'], $modelHierarchy['district'])
            && $this->locationHierarchy['district']
            && $modelHierarchy['district']
            && $this->locationHierarchy['district']->id === $modelHierarchy['district']->id
        ) {
            return ['level' => 'same_district', 'distance' => 1];
        }

        if (
            isset($this->locationHierarchy['city'], $modelHierarchy['city'])
            && $this->locationHierarchy['city']
            && $modelHierarchy['city']
            && $this->locationHierarchy['city']->id === $modelHierarchy['city']->id
        ) {
            return ['level' => 'same_city', 'distance' => 2];
        }

        if (
            isset($this->locationHierarchy['division'], $modelHierarchy['division'])
            && $this->locationHierarchy['division']
            && $modelHierarchy['division']
            && $this->locationHierarchy['division']->id === $modelHierarchy['division']->id
        ) {
            return ['level' => 'same_division', 'distance' => 3];
        }

        if (
            isset($this->locationHierarchy['country'], $modelHierarchy['country'])
            && $this->locationHierarchy['country']
            && $modelHierarchy['country']
            && $this->locationHierarchy['country']->id === $modelHierarchy['country']->id
        ) {
            return ['level' => 'same_country', 'distance' => 4];
        }

        return ['level' => 'different_country', 'distance' => 5];
    }

    private function getLocationProximityBoost(array $locationProximity): float
    {
        if (empty($locationProximity) || !isset($locationProximity['level'])) {
            return 1.0;
        }

        // Use config values for location boosts
        return timebank_config("search_optimization.location_boosts.{$locationProximity['level']}", 1.0);
    }

    /**
     * Processes the given search results and assigns a score to each result based on the cleaned search string.
     *
     * @param array $results The array of search results to process and score.
     * @param string $cleanSearch The cleaned search string used for scoring the results.
     * @return \Illuminate\Support\Collection A collection of processed and scored search results.
     */
    private function processAndScoreResults(array $results, string $cleanSearch): \Illuminate\Support\Collection
    {
        // Track search analytics if enabled
        if (timebank_config('search_optimization.analytics.enabled')) {
            $startTime = microtime(true);
        }

        $extractedData = array_map(function ($result) use ($cleanSearch) {
            $type = strtolower(class_basename($result['model']));
            $locationBoost = $this->getLocationProximityBoost($result['location_proximity'] ?? []);

            $originalScore = $result['score'] ?? 1;

            // Apply model boost only if SearchOptimizationHelper is disabled to avoid double boosting
            $modelBoost = 1;
            if (!timebank_config('search_optimization.enabled')) {
                $modelBoost = timebank_config("main_search_bar.boosted_models.{$type}", 1);
            }

            $boostedScore = $originalScore * $modelBoost * $locationBoost;

            // Use the proper location proximity system from logs and config
            $locationProximity = $result['location_proximity'] ?? [];
            $proximityLevel = $locationProximity['level'] ?? 'no_location';
            
            // Convert level-based boosts to base scores for composite calculation
            $levelToScoreMap = [
                'same_district' => 100,    // Highest priority
                'same_city' => 60,         // High priority
                'same_division' => 30,     // Medium priority
                'same_country' => 10,      // Low priority
                'different_country' => 0,  // No proximity bonus
                'no_location' => 0,        // No location bonus
            ];

            $locationBaseScore = $levelToScoreMap[$proximityLevel] ?? 0;
            $normalizedScore = $boostedScore * 20;
            $compositeScore = $locationBaseScore + $normalizedScore;

            // Extract the field that produced the highlight for context
            $highlightField = $this->getHighlightField($result['highlight'] ?? []);

            return [
                'model' => $result['model'],
                'id' => $result['id'] ?? '',
                'score' => round($compositeScore / 10, 1),
                'original_score' => $originalScore,
                'highlight' => $result['highlight'] ?? [],
                'highlight_field' => $highlightField,
                'suggest' => $this->extractSuggestions($result['highlight'] ?? [], $cleanSearch),
                'location_proximity' => $result['location_proximity'] ?? [],
            ];
        }, $results);

        $sortedResults = collect($extractedData)->sortByDesc('score');

        // Apply SearchOptimizationHelper if enabled
        if (timebank_config('search_optimization.enabled')) {
            $userLocation = $this->locationHierarchy;
            $optimized = SearchOptimizationHelper::optimizeSearchResults(
                $sortedResults->toArray(),
                explode(' ', $cleanSearch),
                $userLocation
            );
            $sortedResults = collect($optimized);
        }

        // Track analytics
        if (timebank_config('search_optimization.analytics.enabled') && isset($startTime)) {
            $executionTime = microtime(true) - $startTime;
            SearchOptimizationHelper::trackLocationSearchAnalytics(
                [], // No specific category IDs in this search
                count($results),
                $executionTime,
                $this->locationHierarchy
            );
        }

        return $sortedResults->take(timebank_config('main_search_bar.search.max_results', 50));
    }

    /**
     * Get the field name that produced the highlight
     */
    private function getHighlightField(array $highlights): string
    {
        if (empty($highlights)) {
            return 'unknown';
        }

        // Check for advanced fields first
        $advancedFields = [
            'tags.contexts.tags.name_',
            'tags.contexts.categories.name_'
        ];

        foreach ($highlights as $field => $value) {
            foreach ($advancedFields as $advancedField) {
                if (strpos($field, $advancedField) !== false) {
                    return 'advanced_field';
                }
            }
        }

        // Return the first field if no advanced fields found
        return array_key_first($highlights) ?? 'unknown';
    }

    /**
     * Extracts suggestion strings from the provided highlights array based on the search term.
     *
     * This method processes the highlights array to generate a list of suggestions
     * that are relevant to the given search term. The suggestions can be used for
     * features such as autocomplete or search recommendations.
     *
     * @param array $highlights An array containing highlight data from which suggestions will be extracted.
     * @param string $searchTerm The term that the user has entered to search for suggestions.
     * @return array An array of suggestion strings relevant to the search term.
     */
    private function extractSuggestions(array $highlights, string $searchTerm): array
    {
        $suggestions = [];

        foreach ($highlights as $field => $fieldHighlights) {
            foreach ($fieldHighlights as $highlight) {
                // Remove highlight tags to get clean text
                $preTag = timebank_config('main_search_bar.search.pre-tags', '<em>');
                $postTag = timebank_config('main_search_bar.search.post-tags', '</em>');

                // Handle array format for tags
                if (is_array($preTag)) {
                    $preTag = $preTag[0] ?? '<em>';
                }
                if (is_array($postTag)) {
                    $postTag = $postTag[0] ?? '</em>';
                }

                $cleanText = str_replace([$preTag, $postTag], '', $highlight);

                // Split by sentences first
                $sentences = preg_split('/[.!?]+/', $cleanText);

                foreach ($sentences as $sentence) {
                    $sentence = trim($sentence);

                    // If sentence contains search term and is reasonable length
                    if (stripos($sentence, $searchTerm) !== false && strlen($sentence) > 3) {
                        // Limit sentence to max 5 words to keep suggestions readable
                        $words = preg_split('/\s+/', $sentence);
                        if (count($words) > 5) {
                            // Find search term and keep 4 words around it
                            $searchPosition = -1;
                            for ($i = 0; $i < count($words); $i++) {
                                if (stripos($words[$i], $searchTerm) !== false) {
                                    $searchPosition = $i;
                                    break;
                                }
                            }

                            if ($searchPosition >= 0) {
                                $start = max(0, $searchPosition - 2);
                                $end = min(count($words) - 1, $start + 3);
                                $sentence = implode(' ', array_slice($words, $start, $end - $start + 1));
                            }
                        }

                        $phrase = trim($sentence, '.,!?;:()[]{}');
                        if (strlen($phrase) > 7) {
                            $suggestions[] = $phrase;
                        }
                    }
                }
            }
        }

        return array_slice(array_unique($suggestions), 0, 5);
    }

    /**
     * Limit highlights to only show the most relevant content field
     * Location fields are excluded from highlighting entirely
     */
    private function limitHighlights(array $highlights): array
    {
        if (empty($highlights)) {
            return [];
        }

        // Priority order for content fields only (location fields no longer included in highlighting)
        $priorityFields = [
            'name',
            'full_name', 
            'about_short_' . app()->getLocale(),
            'about_' . app()->getLocale(),
            'cyclos_skills',
            'tags.contexts.tags.name_' . app()->getLocale(),
            'tags.contexts.categories.name_' . app()->getLocale(),
            'post_translations.title_' . app()->getLocale(),
            'post_translations.excerpt_' . app()->getLocale(),
            'post_translations.content_' . app()->getLocale(),
            'post_category.names.name_' . app()->getLocale(),
            'tag.name_' . app()->getLocale(),
            'call_translations.content_' . app()->getLocale(),
        ];

        // Return highlight from the highest priority field that has highlights
        foreach ($priorityFields as $field) {
            if (isset($highlights[$field]) && !empty($highlights[$field])) {
                return [$field => $highlights[$field]];
            }
        }

        // Final fallback - return first available highlight (should not contain location fields now)
        $firstField = array_key_first($highlights);
        return $firstField ? [$firstField => $highlights[$firstField]] : [];
    }

    /**
     * CRITICAL SECURITY METHOD - XSS PROTECTION
     *
     * Sanitize highlights to prevent XSS attacks while preserving Elasticsearch highlight tags.
     *
     * This method ensures that user-generated content in search highlights is properly escaped,
     * while allowing only the configured highlight wrapper tags (e.g., <span> tags used by Elasticsearch).
     *
     * SECURITY VULNERABILITY FIXED:
     * - User-generated content from profile fields (about, about_short, motivation, website, skills)
     * - Gets indexed in Elasticsearch and returned in search highlights
     * - Without sanitization, malicious HTML/JavaScript executes in victim's browser
     * - This method escapes ALL user content while preserving only safe highlight tags
     *
     * HOW THIS PROTECTION CAN BE BROKEN:
     * 1. DANGER: If you bypass this method and pass highlights directly to the view
     * 2. DANGER: If you modify the placeholder strings to match actual user content
     * 3. DANGER: If you change config pre/post tags to include JavaScript event handlers (e.g., onclick=)
     * 4. DANGER: If you use str_replace with user-controlled input for placeholders
     * 5. DANGER: If you cache highlights BEFORE sanitization (must cache AFTER)
     * 6. DANGER: If you add additional HTML tags to the whitelist without careful review
     * 7. DANGER: If you use {!! !!} syntax on highlights from a different source without sanitization
     *
     * SAFE USAGE:
     * - REQUIRED: Always call this method before caching or displaying highlights
     * - REQUIRED: Only use {!! !!} syntax on highlights that have passed through this method
     * - REQUIRED: Keep pre/post tags simple HTML without attributes that can execute code
     * - REQUIRED: Use unique placeholder strings that cannot appear in user input
     *
     * @param array $highlights Array of highlight strings from Elasticsearch
     * @return array Sanitized highlights safe for HTML output with {!! !!}
     */
    private function sanitizeHighlights(array $highlights): array
    {
        if (empty($highlights)) {
            return [];
        }

        // SECURITY: Get configured highlight tags - these must be reviewed if changed
        // Only simple HTML tags without event handlers are safe (e.g., <span class="...">, not <span onclick="...">)
        $preTag = timebank_config('main_search_bar.search.pre-tags', '<span class="font-semibold text-white leading-tight">');
        $postTag = timebank_config('main_search_bar.search.post-tags', '</span>');

        $sanitized = [];

        foreach ($highlights as $field => $highlightArray) {
            $sanitized[$field] = [];

            foreach ($highlightArray as $highlightText) {
                // SECURITY STEP 1: Temporarily replace Elasticsearch highlight tags with unique placeholders
                // Placeholders must be unique strings that cannot appear in user input
                $placeholder_pre = '___HIGHLIGHT_START___';
                $placeholder_post = '___HIGHLIGHT_END___';

                $safe = str_replace($preTag, $placeholder_pre, $highlightText);
                $safe = str_replace($postTag, $placeholder_post, $safe);

                // SECURITY STEP 2: Escape ALL HTML entities to prevent XSS
                // This converts < > & " ' to HTML entities, making them harmless
                // ENT_QUOTES escapes both single and double quotes
                // ENT_HTML5 ensures proper HTML5 entity encoding
                $safe = htmlspecialchars($safe, ENT_QUOTES | ENT_HTML5, 'UTF-8');

                // SECURITY STEP 3: Restore ONLY the safe Elasticsearch highlight tags
                // At this point, any malicious HTML is escaped, so restoring our known-safe tags is safe
                $safe = str_replace($placeholder_pre, $preTag, $safe);
                $safe = str_replace($placeholder_post, $postTag, $safe);

                $sanitized[$field][] = $safe;
            }
        }

        return $sanitized;
    }

    public function showSearchResults()
    {
        if (strlen($this->search) > 1) {
            $searchTerm = $this->search;
            $total = $this->total;
            $this->results = $this->fetchedResults;

            $key = 'main_search_bar_results_' . Auth::guard('web')->id();
            $results = collect($this->results)->map(fn ($r) => [
                'model'    => $r['model'],
                'id'       => $r['id'],
                'highlight' => $r['highlight'] ?? [],
                'score'    => isset($r['score']) ? round($r['score'] / 10, 1) : null,
                'location_proximity' => $r['location_proximity'] ?? [],
            ])->values()->all();

            cache()->put($key, ['results' => $results, 'searchTerm' => $searchTerm, 'total' => $total], now()->addMinutes(timebank_config('main_search_bar.cache_results', 5)));


            return redirect()->route('search.show');
        } else {
            return null;
        }
    }
}
