<?php

namespace App;

use App\Facades\Feature;
use App\Facades\Settings;
use App\Http\Resources\VehicleList;
use App\Modules\PlaceholderImages\ImaginStudio;
use App\Traits\CacheObject;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Str;
use Mtc\Filter\Contracts\CustomPatternFilter;
use Mtc\Filter\Contracts\FilterObject;
use Mtc\Filter\Contracts\FilterSeoContract;
use Mtc\Filter\Contracts\IsFilter;
use Mtc\Filter\FilterIndex;
use Mtc\MercuryDataModels\Filters\IndexedFilter;
use Mtc\MercuryDataModels\Filters\VehicleAttributeBooleanFilter;
use Mtc\MercuryDataModels\SearchIndex;
use Mtc\MercuryDataModels\SeoData;
use Mtc\MercuryDataModels\SeoDefault;
use Mtc\MercuryDataModels\VehicleAttribute;
use Mtc\MercuryDataModels\VehicleFinance;
use Illuminate\Support\Facades\Lang;
use Mtc\MercuryDataModels\VehicleMake;
use Mtc\MercuryDataModels\VehicleModel;

class Filter extends \Mtc\Filter\Filter
{
    use Traits\MapElementBasedContent;
    use CacheObject;

    protected $applyFiltersToResults = true;

    private ?SeoDefault $seo_default;

    protected Collection $fullIndex;

    /**
     * Filter constructor
     *
     * @param Request $request
     * @param FilterObject $product_handler
     * @param FilterSeoContract $seo
     */
    public function __construct(Request $request, FilterObject $product_handler, FilterSeoContract $seo)
    {
        $this->request = $request;
        $this->seo = $seo;
        $this->config = Config::get('filter');
        $this->product_handler = $product_handler;
        $this->active_sort_option_name = $this->config['default_sort_choice'];
    }

    /**
     * Find the id of the slug
     *
     * @param string $model_type
     * @param $slug
     * @return int
     */
    public static function matchSlugToId(string $model_type, $slug)
    {
        return match ($model_type) {
            'make' => VehicleMake::query()->where('slug', $slug)->firstOrFail()->id,
            'model' => VehicleModel::query()->where('slug', $slug)->firstOrFail()->id,
            default => FilterIndex::query()
                ->where('filter_type', $model_type)
                ->where('slug', $slug)
                ->firstOrFail()
                ->filter_id
        };
    }

    /**
     * Find the id of the slug
     *
     * @param string $model_type
     * @param $slug
     * @return Model
     */
    public static function matchSlugToRecord(string $model_type, $slug)
    {
        return match ($model_type) {
            'make' => VehicleMake::query()->where('slug', $slug)->firstOrFail(),
            'model' => VehicleModel::query()->where('slug', $slug)->firstOrFail(),
            default => FilterIndex::query()
                ->where('filter_type', $model_type)
                ->where('slug', $slug)
                ->firstOrFail()
                ->filter
        };
    }

    /**
     * Parse Request to selections and filter data
     *
     * @return array
     */
    public function parseRequest(): array
    {
        $this->decodeRequest();

        return [
            'seo' => $this->getPageSeoData(),
            'selections' => $this->getSelectionList(),
            'sort_by' => $this->active_sort_option_name,
            'base_url' => url($this->config['url_entrypoint']),
            'page' => $this->request->input('page', 1),
        ];
    }

    /**
     * Handle Ajax request, perform filtering
     *
     * @return array
     */
    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->run();

        $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'),
            ]
        ];
    }

    public function handleWidget()
    {
        $this->request = request();
        $this->applyFiltersToResults = Settings::get('filter-apply-selections-to-results-for-widget', false);
        $this->selections = $this->groupSelectionsByType($this->request->input('selections') ?? []);
        $this->run();
        return [
            'result_count' => $this->getResultCount(false),
            'filters' => $this->getFilterResults(true),
            'url' => $this->getPageUrl(),
        ];
    }

    /**
     * Get enabled filters
     *
     * @return Collection
     */
    public function getFilters(): Collection
    {
        if (empty($this->filters)) {
            $this->filters = collect($this->getConfigFilters())
                ->map(fn($filter_class, $type) => App::make($filter_class, ['type' => $type]))
                ->filter(fn($filter) => $filter instanceof IsFilter)
                ->filter(fn($filter, $name) => Settings::get($this->getSettingsFilterName($name)) === true)
                ->sortBy(fn($filter, $name) => Settings::getOrder($this->getSettingsFilterName($name)));
        }

        return $this->filters;
    }

    /**
     * Fetch the list of supported sort options
     * Returned in format of slug => display name
     *
     * @return array
     */
    public function getSortOptions(): array
    {
        return collect($this->getConfigSortOptions())
            ->filter(fn($filter, $filter_name) => Settings::get("automotive-vehicle-sorting-$filter_name") === true)
            ->sortBy(fn($filter, $name) => Settings::getOrder("automotive-vehicle-sorting-$name"))
            ->map(fn($sort_class, $sort_name) => [
                'code' => $sort_name,
                'label' => $this->getSortOptionName($sort_name)
            ])
            ->values()
            ->toArray();
    }

    /**
     * Get the selections as a flat list
     *
     * @return Collection
     */
    public function getSelectionList(): Collection
    {
        if ($this->index === null) {
            $this->getFilterIndexForSelections();
        }

        return collect($this->selections)
            ->filter(fn($filter_group, $filter_type) => isset($this->getConfigFilters()[$filter_type]))
            ->flatMap(fn($filter_group, $filter_type) => collect($filter_group)
                ->map(function ($value) use ($filter_type) {

                    $to_return = [
                        'type' => $filter_type,
                        'value' => $value,
                        'name' => $this->selectionName($filter_type, $value),
                        'slug' => $value,
                    ];

                    if ($filter_type === 'model') {
                        $to_return['make_slug'] = null;
                        $filter_results = $this->getCachedFilterResults();

                        if (array_key_exists($filter_type, $filter_results)) {
                            $to_return['make_slug'] = collect($filter_results[$filter_type]['results'])
                                ->filter(fn($model_result) => $model_result['slug'] == $value)
                                ->first()['make_slug'] ?? null;
                        }
                    }

                    return $to_return;
                }));
    }

    /**
     * Build the URL of the page that has been viewed
     *
     * @param bool $absolute
     * @return string
     */
    public function getPageUrl(bool $absolute = true): string
    {
        $append = '';
        $customSort = $this->active_sort_option_name !== $this->config['default_sort_choice'];
        $params = array_filter([
            'page' => $this->request->input('page') > 1 ? $this->request->input('page') : null,
            'sort_by' => $customSort ? $this->active_sort_option_name : null,
        ]);
        if (!empty($params)) {
            $append = '?' . http_build_query($params);
        }
        return $this->filterElementsToSlugs($this->selections)->join('/') . $append;
    }


    /**
     * Get the selection name from index
     *
     * @param string $type
     * @param $value
     * @return string|null
     */
    protected function selectionName(string $type, $value): ?string
    {
        $filterInstance = App::make($this->getConfigFilters()[$type], ['type' => $type]);
        $customPatternFilter = $filterInstance instanceof CustomPatternFilter;

        if ($customPatternFilter || method_exists($filterInstance, 'getSelectionName')) {
            return $filterInstance->getSelectionName($value);
        }

        return $this->selectionNameInIndex($type, $value);
    }


    /**
     * Get the selection name from index
     *
     * @param string $type
     * @param $value
     * @return string|null
     */
    protected function selectionNameInIndex(string $type, $value): ?string
    {
        return $this->index
            ->where('filter_type', $type)
            ->firstWhere('slug', $value)
            ?->name;
    }

    /**
     * Create a list of all filter elements (selections, ordering, terms etc.) in one list of slugs
     *
     * @param array $selections
     * @return Collection
     */
    protected function filterElementsToSlugs(array $selections): Collection
    {
        $all = collect([]);
        collect($selections)
            ->sortBy(fn($selection, $type) => array_search($type, array_keys($this->getConfigFilters()), true))
            ->each(function ($type_selections, $type) use ($all) {
                $filter = $this->getFilter($type);
                // Unsupported selection
                if ($filter === null) {
                    return;
                }

                collect($type_selections)->each(function ($selection) use ($all, $filter, $type) {
                    $slugs = $this->getFilterIndexForSelections()
                        ->groupBy('filter_type')
                        ->map(fn(Collection $group) => $group->keyBy('slug'));

                    if ($filter instanceof CustomPatternFilter) {
                        $all->push($filter->createSlug($selection));
                    } elseif (isset($slugs[$type][$selection]['slug'])) {
                        $all->push($slugs[$type][$selection]['slug']);
                    }
                });
            });

        return $all;
    }

    protected function getCachedFilterResults(): array
    {
        if (!empty($this->selections) && count($this->request->input('selections', [])) > 2) {
            return $this->getFilterResults();
        }

        $append = collect($this->request->input('selections', []))
            ->map(fn($selection) => $selection['type'] . '='
                . (is_array($selection['value']) ? implode('|', $selection['value']) : $selection['value']))
            ->implode('-');

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

    /**
     * Retrieve filter results based on selections
     *
     * @param bool $prependAny
     * @return array
     */
    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))
            )
            ->tap(fn(Collection $filters) => $this->eagerLoadIndexedFilters($filters))
            ->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);
    }

    /**
     * Retrieve results for single filter
     *
     * @param IsFilter $filter
     * @param string $filter_name
     * @return Collection
     */
    protected function retrieveSingleFilterResults(Isfilter $filter, string $filter_name): Collection
    {
        $limit = in_array($filter_name, $this->request->input('expanded', []), true)
            ? 0
            : $this->config['filter_limit'];

        if (Feature::isEnabled('indexed-search-page') && $filter instanceof IndexedFilter) {
            if (!empty($this->fullIndex[$filter->filterType()])) {
                return $this->fullIndex[$filter->filterType()];
            }

            return $filter->getIndexedResults(
                $filter->filterType(),
                $limit,
                $this->selections ?? []
            )->map(function ($entry) {
                $entry->id = $entry->filter_id;
                return $entry;
            });
        }

        return parent::retrieveSingleFilterResults($filter, $filter_name);
    }

    protected function eagerLoadIndexedFilters(Collection $filters): void
    {
        if (!Feature::isEnabled('indexed-search-page')) {
            return;
        }
        $filter_keys = $filters->filter(fn($filter) => $filter instanceof IndexedFilter)
            ->reject(fn($filter, $name) => $name == 'model')
            ->map(fn(IndexedFilter $filter) => $filter->filterType());

        $this->fullIndex = SearchIndex::query()
            ->whereIn('filter_type', $filter_keys)
            ->get()
            ->map(function ($entry) {
                $entry->id = $entry->filter_id;
                return $entry;
            })
            ->groupBy('filter_type');
    }

    /**
     * Get (product) results.
     * Sets the orderBy for the main query
     * Executes the query to retrieve data
     * Formats data to defined response format
     *
     * @return int
     */
    protected function getResultCount(bool $include_extras = true): int
    {
        return $this->product_handler->getResultCount(clone $this->query, $include_extras);
    }

    /**
     * Apply filters for other filters.
     * This applies the same  filters as result filtering except for current active filter.
     * It also adds product filter condition
     *
     * @param Builder $query
     * @param string $active_filter
     */
    protected function applyForFilters(Builder $query, string $active_filter): void
    {
        if ($this->applyFiltersToResults) {
            $this->getFilters()
                ->reject(fn(IsFilter $filter, string $type) => $this->filterNotApplicable($active_filter, $type))
                ->each(fn(IsFilter $filter, string $type) => $filter->applyFilter($query, $this->selections[$type]));
        } elseif ($active_filter === 'model') {
            $this->getFilters()
                ->filter(fn(IsFilter $filter, string $type) => !empty($this->selections[$type]) && 'make' === $type)
                ->each(fn(IsFilter $filter, string $type) => $filter->applyFilter($query, $this->selections[$type]));
        }

        $this->product_handler->applyFilter($query);
    }

    protected function filterNotApplicable(string $active_filter, string $type)
    {
        if (empty($this->selections[$type]) || $active_filter === $type) {
            return true;
        }

        if (
            Settings::get('automotive-vehicle-filters-filter-makes-by-selected-model') === false
            && $type == 'model'
            && $active_filter == 'make'
        ) {
            return true;
        }

        // Due to the way how data is fetched for min/max filters we also discard counterpart from
        if (Str::endsWith($active_filter, ['_min', '_max'])) {
            return substr($active_filter, 0, -4) === substr($type, 0, -4);
        }

        return false;
    }


    /**
     * Group selections by type
     * @param array $selections
     * @param string $group_by
     * @return array
     */
    protected function groupSelectionsByType(array $selections = [], string $group_by = 'type'): array
    {
        return collect($selections)
            ->filter(fn($selection) => !empty($selection['value']))
            ->groupBy($group_by)
            ->map(fn($group) => collect($group)->pluck('value'))
            ->toArray();
    }

    private function fillSeo(): array
    {
        if (!empty($this->request->header('x-path'))) {
            $data = SeoData::query()->where('path', $this->request->header('x-path'))->first();
            if ($data) {
                $data->load('mediaUses.media');
                $data->content =  collect($data->content)
                    ->map(fn($entry) => $this->mapContent($data, $entry, $this->request, 'content'))
                    ->toArray();
                return $data->only([
                    'title',
                    'description',
                    'heading',
                    'content',
                    'content_description',
                ]);
            }
        }
        return [
            'title' => $this->seoTitle(),
            'description' => $this->seoDescription(),
            'cheapest_item' => $this->getCheapestOffer(),
        ];
    }

    private function seoTitle(): string
    {
        $default = $this->seoDefault();

        $result = $this->replaceSeoTags([
            '{{MAKE}}' => $this->replaceSelectionByType('make'),
            '{{MODEL}}' => $this->replaceSelectionByType('model'),
            '{{FUEL_TYPE}}' => $this->replaceSelectionByType('fuel_type'),
            '{{BODY_STYLE}}' => $this->replaceSelectionByType('body_type'),
            '{{TRANSMISSION}}' => $this->replaceSelectionByType('transmission'),
            '{{COLOUR}}' => $this->replaceSelectionByType('colour'),
            '{{VEHICLE_TYPE}}' => $this->replaceSelectionByType('vehicle_type'),
            '{{CONDITION}}' => $this->replaceSelectionByType('is_new'),
            '{{CURRENT_PAGE}}' => $this->getCurrentPageText(),
            '{{PAGE}}' => $this->getCurrentPage(),
            '{{SITE_NAME}}' => Settings::get('app-name'),
            '{{NEW_USED}}' => $this->replaceSelectionByType('is_new'),

        ], $default->title ?? '');
        return str_replace(['||', '|  |', '| |'], '|', $result);
    }

    private function seoDescription(): string
    {
        $default = $this->seoDefault();

        $result = $this->replaceSeoTags([
            '{{MAKE}}' => $this->replaceSelectionByType('make'),
            '{{MODEL}}' => $this->replaceSelectionByType('model'),
            '{{FUEL_TYPE}}' => $this->replaceSelectionByType('fuel_type'),
            '{{BODY_STYLE}}' => $this->replaceSelectionByType('body_type'),
            '{{TRANSMISSION}}' => $this->replaceSelectionByType('transmission'),
            '{{COLOUR}}' => $this->replaceSelectionByType('colour'),
            '{{VEHICLE_TYPE}}' => $this->replaceSelectionByType('vehicle_type'),
            '{{CONDITION}}' => $this->replaceSelectionByType('is_new'),
            '{{CURRENT_PAGE}}' => $this->getCurrentPageText(),
            '{{PAGE}}' => $this->getCurrentPage(),
            '{{SITE_NAME}}' => Settings::get('app-name'),
            '{{NEW_USED}}' => $this->replaceSelectionByType('is_new'),

        ], $default->description ?? '');

        return str_replace(['||', '|  |', '| |'], '|', $result);
    }

    /**
     * Get the current pagination page of the filter
     *
     * @return int
     */
    public function getCurrentPage(): int
    {
        return (int)$this->request->input('page', 1);
    }

    public function getCurrentPageText(): string
    {
        $page = (int)$this->request->input('page', 1);

        return $page > 1
            ? __('pagination.current_page', ['page' => $page])
            : '';
    }

    private function seoDefault(): ?SeoDefault
    {
        if (!isset($this->seo_default)) {
            $this->seo_default = SeoDefault::query()->where('section', 'filter')->first();
        }
        return $this->seo_default;
    }

    private function replaceSelectionByType(string $type): string
    {
        if (empty($this->selections[$type]) || count($this->selections[$type]) > 1) {
            return '';
        }

        if ($type == 'is_new') {
            return ucfirst($this->selections[$type][0] ?? '');
        }

        return $this->selectionNameInIndex($type, $this->selections[$type][0]) ?? '';
    }

    private function replaceSeoTags(array $replacements, string $string): string
    {
        return str_replace(array_keys($replacements), $replacements, $string);
    }

    /**
     * @return Builder[]|\Illuminate\Database\Eloquent\Collection|Collection
     */
    protected function getFilterIndexForSelections(): Collection
    {
        if (empty($this->index)) {
            $index_query = FilterIndex::query();
            collect($this->selections)
                ->reject(fn($selections, $type) => $this->getCustomPatternFilters()->has($type))
                ->each(function ($selections, $type) use ($index_query) {
                    $index_query->orWhere(function ($selection_query) use ($type, $selections) {
                        $selection_query->where('filter_type', $type)->whereIn('slug', $selections);
                    });
                });
            $this->index = $index_query
                ->get();
        }

        return $this->index;
    }

    protected function matchSelectedFilters(): void
    {
        if (empty($this->filter_url_elements)) {
            return;
        }

        $matched = FilterIndex::query()
            ->whereIn('slug', $this->filter_url_elements)
            ->get();

        $this->filter_url_elements = array_diff($this->filter_url_elements, $matched->pluck('slug')->toArray());

        $this->selections = collect($matched)
            ->groupBy('filter_type')
            ->map(fn($group) => collect($group)->pluck('slug'))
            ->toArray();
    }

    /**
     * @param VehicleList $results
     * @return mixed
     */
    public function getRepresentativeFinance(JsonResource $results): ?VehicleFinance
    {
        $items = method_exists($results->resource, 'items')
            ? $results->resource->items()
            : $results->resource;

        foreach ($items as $item) {
            if (
                $item->relationLoaded('defaultFinanceExample')
                && !empty($item->defaultFinanceExample)
                && $item->defaultFinanceExample->monthly_price > 0
            ) {
                return $item->defaultFinanceExample;
            }
        }

        return null;
    }

    protected function groupRangeFilters($filters)
    {
        $filters_to_return = [];

        foreach ($filters as $filter_name => $filter) {
            if (
                $filter['ui_component'] !== 'range-filter'
                || !array_key_exists('is_range_maximum', $filter)
                || empty($filter['range_group'])
            ) {
                $filters_to_return[$filter_name] = $filter;
                continue;
            }

            if (!array_key_exists($filter['range_group'], $filters_to_return)) {
                $filters_to_return[$filter['range_group']] = $filter;
                $filters_to_return[$filter['range_group']]['results'] = [];
            }

            if ($filter['is_range_maximum']) {
                $filters_to_return[$filter['range_group']]['results']['filter_max'] = $filter['results'];
                $filters_to_return[$filter['range_group']]['search_term_max'] = $filter_name;
            } else {
                $filters_to_return[$filter['range_group']]['results']['filter_min'] = $filter['results'];
                $filters_to_return[$filter['range_group']]['search_term_min'] = $filter_name;
            }

            unset($filters_to_return[$filter['range_group']]['is_range_maximum']);
        }

        return $filters_to_return;
    }

    protected function getConfigFilters(): array
    {
        $additionalFilters = $this->cache('additional-filters', 15, fn() => $this->getAdditionalFilters());

        return array_merge($this->config['filters'], $additionalFilters);
    }

    protected function getConfigSortOptions(): array
    {
        $additional_sort_options = $this->cache(
            'additional-sort-options',
            15,
            fn() => $this->product_handler->additionalSortOptions()
        );
        return array_merge(
            $this->config['sort_options'][$this->product_handler->sortOptionName()],
            $additional_sort_options
        );
    }

    protected function getSettingsFilterName(string $name): string
    {
        return "automotive-vehicle-filters-$name";
    }

    protected function setSorting($query)
    {
        return App::make(
            $this->getConfigSortOptions()[$this->active_sort_option_name]
        )->handle($query, $this->active_sort_option_name);
    }

    protected function getSortOptionName($sort_name): string
    {
        $translationKey = "filter::filter.sort_options.$sort_name";
        if (Lang::has($translationKey)) {
            return __($translationKey);
        }

        $vehicleAttribute = VehicleAttribute::query()
            ->where('sortable', 1)
            ->where('slug', $sort_name)
            ->value('name');

        return $vehicleAttribute ?: '';
    }

    protected function getAdditionalFilters(): array
    {
        $filters = [];

        $filterableAttributes = VehicleAttribute::query()
            ->where('filterable', 1)
            ->get();

        foreach ($filterableAttributes as $filterableAttribute) {
            $class = match ($filterableAttribute->type) {
                'boolean' => VehicleAttributeBooleanFilter::class,
                default => null
            };

            if ($class) {
                $filters[$filterableAttribute->slug] = $class;
            }
        }

        return $filters;
    }

    private function getCheapestOffer(): ?float
    {
        return $this->query->min('price');
    }
}
