<?php

namespace App;

use App\Facades\Settings;
use App\Http\Resources\StockAndOfferList;
use Illuminate\Database\Concerns\BuildsQueries;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Config;
use Mtc\Filter\Contracts\IsFilter;

class StockAndOfferFilter extends Filter
{
    use BuildsQueries;

    private $sort_orders = null;

    /**
     * Retrieve filter results based on selections
     *
     * @param bool $prependAny
     * @return array
     */
    #[\Override]
    protected function getFilterResults(bool $prependAny = false): array
    {
        $filters_stock = $this->searchIsForOffersOnly()
            ? []
            : $this->getFilterResultsForFilter($this->getFiltersFromConfig('filters'));

        $filters_offers = $this->searchIsForStockOnly()
            ? []
            : $this->getFilterResultsForFilter($this->getFiltersFromConfig('offer_filters'));

        return $this->getMergedFilters($filters_stock, $filters_offers)->toArray();
    }

    protected function getFiltersFromConfig(string $filter_key): Collection
    {
        $prefix = $filter_key == 'offer_filters' ? 'offer-' : '';

        return collect($this->config[$filter_key])
                ->map(fn ($filter_class) => App::make($filter_class))
                ->filter(fn ($filter) => $filter instanceof IsFilter)
                ->filter(fn ($filter, $name) => Settings::get($this->getSettingsFilterName($prefix . $name)) === true)
                ->sortBy(fn ($filter, $name) => Settings::getOrder($this->getSettingsFilterName($prefix . $name)));
    }

    protected function getFilterResultsForFilter(Collection $filters_in, bool $prependAny = false): array
    {
        $this->selections = array_merge(
            $this->selections,
            collect($this->selections)
                ->filter(fn ($selection, $key) => in_array($key, array_keys($filters_in->toArray())))->toArray()
        );

        $input = $this->request->input();

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

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

    /**
     * Retrieve results for single filter
     *
     * @param IsFilter $filter
     * @param string $filter_name
     * @return Collection
     */
    #[\Override]
    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 (in_array($filter::class, $this->config['offer_filters'])) {
            $filter_source = 'offer_filters';
        } else {
            $filter_source = 'filters';
        }

        return $filter->getResults(
            function ($query) use ($filter_name, $filter_source) {
                $this->applyForFiltersFromSource($query, $filter_name, $filter_source);
            },
            $limit,
            $this->selections[$filter_name] ?? []
        );
    }

    protected function getQuery(string $filter_source): Builder
    {
        $this->request = request();

        if ($filter_source == 'offer_filters') {
            $this->query = (App::make(OfferFilterObject::class))->createQuery();
        } else {
            $this->query = $this->createQuery();
        }

        $this->applyForResultsFromSource($filter_source);
        $this->matchSortFromAjaxRequest();

        return $this->query;
    }

    protected function applyForResultsFromSource(string $filter_source): void
    {
        $this->getFiltersFromConfig($filter_source)
            ->reject(fn (IsFilter $filter, string $type) => empty($this->selections[$type]))
            ->each(fn (IsFilter $filter, string $type) => $filter->applyFilter($this->query, $this->selections[$type]));
    }

    /**
     * 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 applyForFiltersFromSource(Builder $query, string $active_filter, string $filter_source): void
    {
        if ($this->applyFiltersToResults) {
            $this->getFiltersFromConfig($filter_source)
                ->reject(
                    fn (IsFilter $filter, string $type) => empty($this->selections[$type])
                        || $active_filter === $type
                        || (
                            Settings::get('automotive-vehicle-filters-filter-makes-by-selected-model') === false
                            && $type == 'model'
                            && $active_filter == 'make'
                        )
                )
                ->each(fn (IsFilter $filter, string $type) => $filter->applyFilter($query, $this->selections[$type]));
        } elseif ($active_filter === 'model') {
            $this->getFiltersFromConfig($filter_source)
                ->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 format(\Illuminate\Contracts\Pagination\LengthAwarePaginator $results): JsonResource
    {
        return new StockAndOfferList($results);
    }

    #[\Override]
    protected function getResults(): JsonResource
    {
        $stock_query = $this->getQuery('filters');
        $offer_query = $this->getQuery('offer_filters');

        // If the search is for offers only then we don't want to return any vehicles,
        // however we must query the vehicles table to ensure the result set contains Vehicle objects
        if ($this->searchIsForOffersOnly()) {
            $stock_query->where('id', '<', 0);
        }

        if ($this->searchIsForStockOnly()) {
            $offer_query->where('id', '<', 0);
        }

        // Reset any columns already set on the queries (e.g. the offer query selects
        // vehicle_offers.* plus subquery columns via withMatchingVehicleCount()) so that
        // selectRaw() defines the exact column list and both sides of the UNION match.
        // The matching select bindings must be cleared too, otherwise stale bindings from
        // the removed subquery columns shift the remaining bindings out of alignment.
        foreach ([$stock_query, $offer_query] as $query) {
            $query->getQuery()->columns = null;
            $query->getQuery()->bindings['select'] = [];
        }

        $union_query = $stock_query->selectRaw(implode(',', $this->getSelectColumnsForStock()))
            ->union($offer_query->selectRaw(implode(',', $this->getSelectColumnsForOffers())));

        if ($this->shouldApplyStockProviderSort()) {
            $union_query->orderBy('stock_provider_sort_index');
            $this->stockProviderSortApplied = true;
        }

        if (
            Settings::get('filter-results-order-by-vehicle-type') === true
            && $this->selectionsHaveVehicleTypeSorting()
        ) {
            // sort initially by vehicle types.
            // subsequent sorts should be appended to the query.
            $union_query->orderBy('vehicle_type_sort_index');
        }

        // get the sorting from the stock filter
        $union_query = Filter::setSorting($union_query);

        return $this->format($union_query->paginate($this->getPageLimit()));
    }

    protected function getPageLimit(): int
    {
        $pageLimit = Settings::get('filter-results-per-page', Config::get('filter.result_page_limit'));
        return $this->request->get('perPage', $pageLimit);
    }

    /**
     * Return columns to retrieve.
     * Columns should include any columns required for ordering.
     * @return string[]
     */
    protected function getSelectColumnsForStock(): array
    {
        $columns = [
            'id',
            'created_at',
            'published_at',
            'derivative',
            'make_id',
            'model_id',
            'transmission_id',
            'fuel_type_id',
            'body_style_id',
            'slug',
            'price',
            'manufacture_year',
            'monthly_price',
            'is_offer',
            'colour',
            'dealership_id',
            'trim',
            'is_vat_applicable',
            'title',
            'original_price',
            'previous_price',
            'rrp_price',
            'is_new',
            'type',
        ];

        $columns[] = Settings::get('automotive-distance_measurement') === 'mi' ? 'odometer_mi' : 'odometer_km';

        // set the array keys, required later.
        $columns = array_combine($columns, $columns);

        $columns['is_offer'] = '0 as is_offer';

        if (
            Settings::get('filter-results-order-by-vehicle-type') === true
            && $this->selectionsHaveVehicleTypeSorting()
        ) {
            $columns['vehicle_type_sort_index'] = '(' . $this->getStockStatusQuery() . ') as vehicle_type_sort_index';
        }

        if ($this->shouldApplyStockProviderSort()) {
            $columns['stock_provider_sort_index'] = '('
                . $this->buildStockProviderCaseExpression()
                . ') as stock_provider_sort_index';
        }

        return $columns;
    }

    protected function getSelectColumnsForOffers(): array
    {
        $columns = $this->getSelectColumnsForStock();
        $odometer = Settings::get('automotive-distance_measurement') === 'mi' ? 'odometer_mi' : 'odometer_km';

        if (array_key_exists($odometer, $columns)) {
            $columns[$odometer] = '0 AS ' . $odometer;
        }

        if (array_key_exists('is_vat_applicable', $columns)) {
            $columns['is_vat_applicable'] = '1 AS is_vat_applicable';
        }

        if (array_key_exists('title', $columns)) {
            $columns['title'] = 'name AS title';
        }

        if (array_key_exists('original_price', $columns)) {
            $columns['original_price'] = 'null AS original_price';
        }

        if (array_key_exists('previous_price', $columns)) {
            $columns['previous_price'] = 'null AS previous_price';
        }

        if (array_key_exists('rrp_price', $columns)) {
            $columns['rrp_price'] = 'null AS rrp_price';
        }

        if (array_key_exists('manufacture_year', $columns)) {
            $columns['manufacture_year'] = date('Y') . ' AS manufacture_year';
        }

        if (array_key_exists('is_new', $columns)) {
            $columns['is_new'] = 'true AS is_new';
        }

        if (array_key_exists('monthly_price', $columns)) {
            // this doesn't seem to work with query builder ->toSql()
            $columns['monthly_price'] = '(
                SELECT monthly_price
                FROM vehicle_offer_finance
                WHERE vehicle_offer_finance.offer_id = vehicle_offers.id
                ORDER BY monthly_price ASC LIMIT 1
                ) AS monthly_price';
        }

        if (array_key_exists('is_offer', $columns)) {
            $columns['is_offer'] = '1 as is_offer';
        }

        if (
            Settings::get('filter-results-order-by-vehicle-type') === true
            && $this->selectionsHaveVehicleTypeSorting()
            && array_key_exists('vehicle_type_sort_index', $columns)
        ) {
            $columns['vehicle_type_sort_index'] = $this->getSortOrderByVehicleStatus('offer')
                . ' as vehicle_type_sort_index';
        }

        if (
            $this->shouldApplyStockProviderSort()
            && array_key_exists('stock_provider_sort_index', $columns)
        ) {
            $columns['stock_provider_sort_index'] = self::SORT_INDEX_LAST . ' as stock_provider_sort_index';
        }

        return $columns;
    }

    protected function getMergedFilters(array $stock_filter_result, array $offer_filter_result): Collection
    {
        if (empty($stock_filter_result)) {
            return collect($offer_filter_result);
        }

        if (empty($offer_filter_result)) {
            return collect($stock_filter_result);
        }

        return collect($stock_filter_result)
            ->map(function ($stock_filter, $filter_name) use ($offer_filter_result) {
                if (array_key_exists($filter_name, $offer_filter_result)) {
                    $stock_filter_results = collect($stock_filter['results'])
                        ->mapWithKeys(function ($item, $key) use ($filter_name) {
                            if (is_array($item) && array_key_exists('id', $item)) {
                                return [
                                    $item['id'] => $item
                                ];
                            }

                            return [
                                $key => $item
                            ];
                        });

                    $offer_filter_results = collect($offer_filter_result[$filter_name]['results'])
                        ->mapWithKeys(function ($item) {

                            if (is_array($item) && array_key_exists('id', $item)) {
                                return [
                                    $item['id'] => $item
                                ];
                            }

                            return [];
                        });

                    $merged_filter_results = $stock_filter_results->merge($offer_filter_results);

                    $stock_filter['results'] = $merged_filter_results->has('filter_max')
                        ? $merged_filter_results->toArray()
                        : $merged_filter_results->values()->toArray();
                }

                return $stock_filter;
            });
    }

    protected function searchIsForOffersOnly(): bool
    {
        return !empty($this->selections)
            && array_key_exists('stock_status', $this->selections)
            && !in_array('in-stock', $this->selections['stock_status'])
            && !in_array('available-now', $this->selections['stock_status'])
            && !in_array('coming-soon', $this->selections['stock_status']);
    }

    protected function searchIsForStockOnly(): bool
    {
        if (empty($this->selections)) {
            return false;
        }

        if (
            $this->selectionsHaveUsedStatus()
            && !$this->selectionsHaveNewStatus()
        ) {
            return true;
        }

        return array_key_exists('stock_status', $this->selections)
            && !$this->selectionsHaveOfferStockStatus();
    }

    protected function selectionsHaveOfferStockStatus(): bool
    {
        return array_key_exists('stock_status', $this->selections)
            && in_array('available-to-order', $this->selections['stock_status']);
    }

    protected function selectionsHaveNewStatus(): bool
    {
        return array_key_exists('is_new', $this->selections)
            && in_array('new', $this->selections['is_new']);
    }

    protected function selectionsHaveUsedStatus(): bool
    {
        return array_key_exists('is_new', $this->selections)
            && in_array('used', $this->selections['is_new']);
    }

    protected function selectionsHaveVehicleTypeSorting(): bool
    {
        return array_key_exists('use_vehicle_type_sorting', $this->selections)
            && in_array('true', $this->selections['use_vehicle_type_sorting']);
    }

    protected function getSortOrderByVehicleStatus(string $vehicle_status): int
    {
        $sort_orders = $this->getSortOrders();

        if (empty($sort_orders)) {
            return self::SORT_INDEX_LAST;
        }

        foreach ($sort_orders as $sort_order) {
            if ($sort_order['name'] == $vehicle_status) {
                return $sort_order['value'];
            }
        }

        return self::SORT_INDEX_LAST;
    }

    protected function getSortOrders(): ?array
    {
        if (empty($this->sort_orders)) {
            $sort_orders = Settings::get('filter-results-status-order-values', '');

            if (!empty($sort_orders)) {
                $pairs = explode(',', $sort_orders);
                foreach ($pairs as $pair) {
                    [$name, $value] = explode(':', $pair);
                    $this->sort_orders[] = [
                        'value' => trim($value),
                        'name' => trim($name)
                    ];
                }
            }
        }

        return $this->sort_orders;
    }

    protected function getStockStatusQuery(): string
    {
        if (empty($this->getSortOrders())) {
            return self::SORT_INDEX_LAST;
        }

        $stock_status_query = "
            CASE (
                    SELECT `value`
                    FROM vehicle_attribute_values
                    WHERE owner_id = vehicles.id
                    AND owner_type = 'vehicle'
                    AND slug = 'stock_status'
                    LIMIT 1
                )
            ";

        collect($this->getSortOrders())->map(function ($status_value) use (&$stock_status_query) {
            $stock_status_query .= "WHEN '" . $status_value['name'] . "' THEN " . $status_value['value'] . " ";
        });

        $stock_status_query .= "ELSE " . self::SORT_INDEX_LAST . "
            END";

        return $stock_status_query;
    }
}
