<?php

namespace App\Services;

use Illuminate\Support\Facades\Config;
use Meilisearch\Client;
use Meilisearch\Endpoints\Indexes;

class MeilisearchService
{
    private ?Client $client = null;

    public function getClient(): Client
    {
        if ($this->client === null) {
            $this->client = new Client(
                Config::get('meilisearch.host'),
                Config::get('meilisearch.key')
            );
        }

        return $this->client;
    }

    public function getVehicleIndex(): Indexes
    {
        $indexName = $this->getVehicleIndexName();
        return $this->getClient()->index($indexName);
    }

    public function getVehicleIndexName(): string
    {
        $baseName = Config::get('meilisearch.vehicle_index.name', 'vehicles');
        return tenant('id') . '_' . $baseName;
    }

    public function searchVehicles(
        array $filters = [],
        array $sort = [],
        int $limit = 24,
        int $offset = 0,
        ?string $searchTerm = null,
        ?string $geoFilter = null
    ): array {
        $index = $this->getVehicleIndex();

        $filterString = $this->buildFilterString($filters);

        // Append geo filter if provided
        if ($geoFilter) {
            $filterString = $filterString ? "{$filterString} AND {$geoFilter}" : $geoFilter;
        }

        $searchParams = [
            'filter' => $filterString,
            'sort' => $sort,
            'limit' => $limit,
            'offset' => $offset,
        ];

        $results = $index->search($searchTerm ?? '', $searchParams);

        return [
            'hits' => $results->getHits(),
            'total' => $results->getEstimatedTotalHits(),
            'processingTimeMs' => $results->getProcessingTimeMs(),
        ];
    }

    public function getFacets(
        array $filters = [],
        array $facetAttributes = [],
        ?string $searchTerm = null,
        ?string $geoFilter = null
    ): array {
        $index = $this->getVehicleIndex();

        if (empty($facetAttributes)) {
            $facetAttributes = Config::get('meilisearch.vehicle_index.filterable_attributes', []);
        }

        $filterString = $this->buildFilterString($filters);

        // Append geo filter if provided
        if ($geoFilter) {
            $filterString = $filterString ? "{$filterString} AND {$geoFilter}" : $geoFilter;
        }

        $searchParams = [
            'filter' => $filterString,
            'facets' => $facetAttributes,
            'limit' => 0,
        ];

        $results = $index->search($searchTerm ?? '', $searchParams);

        return $results->getFacetDistribution() ?? [];
    }

    public function getFacetsWithStats(
        array $filters = [],
        array $facetAttributes = [],
        ?string $searchTerm = null,
        ?string $geoFilter = null
    ): array {
        $index = $this->getVehicleIndex();

        if (empty($facetAttributes)) {
            $facetAttributes = Config::get('meilisearch.vehicle_index.filterable_attributes', []);
        }

        $filterString = $this->buildFilterString($filters);

        if ($geoFilter) {
            $filterString = $filterString ? "{$filterString} AND {$geoFilter}" : $geoFilter;
        }

        $searchParams = [
            'filter' => $filterString,
            'facets' => $facetAttributes,
            'limit' => 0,
        ];

        $results = $index->search($searchTerm ?? '', $searchParams);

        return [
            'distribution' => $results->getFacetDistribution() ?? [],
            'stats' => $results->getFacetStats() ?? [],
        ];
    }

    public function getVehicleIds(
        array $filters = [],
        array $sort = [],
        int $limit = 1000,
        int $offset = 0,
        ?string $searchTerm = null,
        ?string $geoFilter = null
    ): array {
        $index = $this->getVehicleIndex();

        $filterString = $this->buildFilterString($filters);

        // Append geo filter if provided
        if ($geoFilter) {
            $filterString = $filterString ? "{$filterString} AND {$geoFilter}" : $geoFilter;
        }

        $searchParams = [
            'filter' => $filterString,
            'sort' => $sort,
            'limit' => $limit,
            'offset' => $offset,
            'attributesToRetrieve' => ['id'],
        ];

        $results = $index->search($searchTerm ?? '', $searchParams);

        return [
            'ids' => collect($results->getHits())->pluck('id')->toArray(),
            'total' => $results->getEstimatedTotalHits(),
        ];
    }

    protected function buildFilterString(array $filters): string
    {
        $filterParts = [];

        foreach ($filters as $field => $value) {
            if (empty($value) && $value !== 0 && $value !== false) {
                continue;
            }

            if (is_array($value)) {
                if (isset($value['operator'])) {
                    // Range filter: ['operator' => '>=', 'value' => 1000]
                    $filterParts[] = "{$field} {$value['operator']} {$value['value']}";
                } elseif (isset($value['min']) || isset($value['max'])) {
                    // Min/max filter
                    if (isset($value['min'])) {
                        $filterParts[] = "{$field} >= {$value['min']}";
                    }
                    if (isset($value['max'])) {
                        $filterParts[] = "{$field} <= {$value['max']}";
                    }
                } else {
                    // Array of values (IN filter)
                    $quotedValues = array_map(function ($v) {
                        return is_numeric($v) ? $v : '"' . $this->escapeFilterValue($v) . '"';
                    }, $value);
                    $filterParts[] = "{$field} IN [" . implode(', ', $quotedValues) . "]";
                }
            } elseif (is_bool($value)) {
                $filterParts[] = "{$field} = " . ($value ? 'true' : 'false');
            } elseif (is_numeric($value)) {
                $filterParts[] = "{$field} = {$value}";
            } else {
                $filterParts[] = "{$field} = \"" . $this->escapeFilterValue($value) . "\"";
            }
        }

        return implode(' AND ', $filterParts);
    }

    /**
     * Escape a string value for safe inclusion inside a double-quoted Meilisearch filter literal.
     *
     * Without this, a value containing a double quote can terminate the literal early and inject
     * arbitrary filter expressions (e.g. `OR is_published = false`), exposing unpublished stock.
     * Backslashes are escaped first so the escape character itself cannot be smuggled in.
     */
    protected function escapeFilterValue(mixed $value): string
    {
        return str_replace(['\\', '"'], ['\\\\', '\\"'], (string) $value);
    }

    public function configureVehicleIndex(): void
    {
        $client = $this->getClient();
        $indexName = $this->getVehicleIndexName();

        $client->createIndex($indexName, ['primaryKey' => 'id']);

        $index = $client->index($indexName);

        $index->updateFilterableAttributes(
            Config::get('meilisearch.vehicle_index.filterable_attributes', [])
        );

        $index->updateSortableAttributes(
            Config::get('meilisearch.vehicle_index.sortable_attributes', [])
        );

        $index->updateSearchableAttributes(
            Config::get('meilisearch.vehicle_index.searchable_attributes', [])
        );

        // Set pagination limit to support large inventories (default is 1000)
        $index->updatePagination([
            'maxTotalHits' => Config::get('meilisearch.vehicle_index.max_total_hits', 1_000_000),
        ]);
    }

    public function indexVehicle(array $vehicleData): void
    {
        $index = $this->getVehicleIndex();
        $index->addDocuments([$vehicleData]);
    }

    public function indexVehicles(array $vehiclesData): void
    {
        $index = $this->getVehicleIndex();
        $index->addDocuments($vehiclesData);
    }

    public function deleteVehicle(int $vehicleId): void
    {
        $index = $this->getVehicleIndex();
        $index->deleteDocument($vehicleId);
    }

    public function deleteAllVehicles(): void
    {
        $index = $this->getVehicleIndex();
        $index->deleteAllDocuments();
    }
}
