<?php

namespace App\Console\Commands;

use App\Facades\Feature;
use App\Facades\Settings;
use App\Facades\Site;
use App\Filter\FilterIndex;
use App\Sitemap\QueryParamFilterRegistry;
use Closure;
use Illuminate\Console\Command;
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Storage;
use Mtc\MercuryDataModels\CarConfiguratorModel;
use Mtc\MercuryDataModels\Contracts\ModelWithUrlPath;
use Mtc\MercuryDataModels\Dealership;
use Mtc\MercuryDataModels\Filters\IsFilter;
use Mtc\MercuryDataModels\Franchise;
use Mtc\MercuryDataModels\NewCar;
use Mtc\MercuryDataModels\OfferType;
use Mtc\MercuryDataModels\Page;
use Mtc\MercuryDataModels\Property;
use Mtc\MercuryDataModels\PropertyCategory;
use Mtc\MercuryDataModels\SearchFacet;
use Mtc\MercuryDataModels\Vehicle;
use Mtc\MercuryDataModels\VehicleMake;
use Mtc\MercuryDataModels\VehicleModel;
use Mtc\MercuryDataModels\VehicleOffer;
use Spatie\Sitemap\Sitemap;
use Spatie\Sitemap\SitemapIndex;
use Spatie\Sitemap\Tags\Sitemap as SitemapTag;
use Spatie\Sitemap\Tags\Url;

class MakeSitemap extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'make:sitemap';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Generate sitemap.xml for a site';

    public const SECTION_PAGES = 'pages';
    public const SECTION_OFFERS = 'offers';
    public const SECTION_NEW_VEHICLES = 'new-vehicles';
    public const SECTION_DEALERSHIPS = 'dealerships';
    public const SECTION_VEHICLES = 'vehicles';
    public const SECTION_SEARCH_RESULTS = 'search-results';

    /** @var array<string, Sitemap> */
    private array $sections = [];

    /** @var array<string, true> URLs already emitted to any section, deduped across sections */
    private array $emittedUrls = [];

    private string $wwwPrefix;

    private array $excludePatterns = [];

    /** @var list<string> page_type category slugs (blog, news, events, guides, …) */
    private array $pageTypeCategories = [];

    /** @var array<string, Collection<int, string>> */
    private array $activeVehicleTaxonomyCache = [];

    /**
     * Execute the console command.
     */
    public function handle()
    {
        if (!Feature::isEnabled('sitemap')) {
            return self::SUCCESS;
        }

        $this->sections = [];
        $this->emittedUrls = [];
        $this->wwwPrefix = Settings::get('seo-sitemap-www-prefix', 'as-is');
        $this->excludePatterns = collect(explode("\n", Settings::get('seo-sitemap-exclude-urls', '')))
            ->map(fn ($pattern) => trim($pattern))
            ->filter()
            ->values()
            ->all();
        $this->pageTypeCategories = collect(config('pages.page_types', []))
            ->pluck('value')
            ->filter()
            ->values()
            ->all();

        Page::query()->active()->get()
            ->each(fn (Page $page) => $this->addPageToSitemap($page));

        if (Feature::isEnabled('new-vehicles')) {
            NewCar::query()->active()->with('franchise')->get()
                ->each(fn (NewCar $page) => $this->addNewCarToSitemap($page));

            VehicleOffer::query()->active()->with(['franchise', 'offerType'])->get()
                ->each(fn (VehicleOffer $page) => $this->addOfferToSitemap($page));

            $this->addOfferListingPages();
        }

        if (Feature::isEnabled('dealerships')) {
            Dealership::query()->where('active', 1)->get()
                ->each(fn (Dealership $page) => $this->addModelUrl(self::SECTION_DEALERSHIPS, $page));
        }

        if (Settings::get('seo-sitemap-include-srp')) {
            $facets = $this->loadFacets();
            foreach ($this->resolveSrpBasePaths() as $base) {
                if ($facets->isNotEmpty()) {
                    $this->addFacetedSrpUrls($base['path'], $base['query'], $facets);
                } else {
                    $this->addLegacyHierarchyUrls($base['path'], $base['query']);
                }
            }
        }

        if (Settings::get('seo-sitemap-include-fpa')) {
            Vehicle::query()->active()->get()
                ->each(fn (Vehicle $page) => $this->addModelUrl(self::SECTION_VEHICLES, $page));
        }

        if (Feature::isEnabled('car-configurator')) {
            CarConfiguratorModel::query()->where('active', 1)->get()
                ->each(fn (CarConfiguratorModel $page) => $this->addModelUrl(self::SECTION_PAGES, $page));
        }

        $this->addFixedUrls();
        $this->addPropertyCategoryEntries();
        $this->addPageCategoryPagination();

        $this->writeSitemaps();

        return self::SUCCESS;
    }

    /**
     * Add fixed URLs configured via the seo-sitemap-fixed-urls setting to the
     * pages section.
     */
    private function addFixedUrls(): void
    {
        collect(explode("\n", Settings::get('seo-sitemap-fixed-urls', '')))
            ->map(fn ($url) => trim($url))
            ->filter()
            ->each(function ($url) {
                if ($this->isExcluded($url)) {
                    return;
                }

                $isFullUrl = str_starts_with($url, 'http://') || str_starts_with($url, 'https://');
                $sitemapUrl = $isFullUrl ? $url : $this->sitemapUrl($url);

                $this->addUrl(
                    self::SECTION_PAGES,
                    Url::create($sitemapUrl)->setPriority($this->calculatePriority($url))
                );
            });
    }

    /**
     * Add property entries from categories that have sitemap enabled.
     */
    private function addPropertyCategoryEntries(): void
    {
        PropertyCategory::query()
            ->where('active', 1)
            ->get()
            ->filter(fn (PropertyCategory $category) => ($category->data['sitemap_enabled'] ?? false))
            ->each(function (PropertyCategory $category) {
                $basePath = ltrim($category->data['sitemap_base_path'] ?? $category->slug, '/');

                $propertyCount = $category->properties()
                    ->where('active', 1)
                    ->count();

                $this->addPaginatedUrls(self::SECTION_PAGES, '/' . $basePath, $propertyCount, 15);

                $category->properties()
                    ->where('active', 1)
                    ->get()
                    ->each(function (Property $property) use ($basePath) {
                        $path = '/' . $basePath . '/' . $property->slug;

                        if ($this->isExcluded($path)) {
                            return;
                        }

                        $this->addUrl(
                            self::SECTION_PAGES,
                            Url::create($this->sitemapUrl($path))
                                ->setLastModificationDate($property->updated_at)
                                ->setPriority($this->calculatePriority($path))
                        );
                    });
            });
    }

    /**
     * Add pagination URLs for page category listings (e.g. /news, /offers).
     */
    private function addPageCategoryPagination(): void
    {
        Page::query()->active()
            ->whereNotNull('category')
            ->where('category', '!=', '')
            ->selectRaw('category, COUNT(*) as total')
            ->groupBy('category')
            ->get()
            ->each(function ($row) {
                $this->addPaginatedUrls(
                    $this->sectionForCategory($row->category),
                    '/' . $row->category,
                    $row->total,
                    15
                );
            });
    }

    /**
     * Add offer listing pages to the sitemap based on site configuration.
     */
    private function addOfferListingPages(): void
    {
        $basePath = $this->getOfferListingBasePath();
        $perPage = (int) (Settings::get('filter-offers-per-page') ?? 15);

        if (Settings::get('seo-sitemap-offers-top-level-listing', true)) {
            $this->addListingUrl(self::SECTION_OFFERS, $basePath);
            $totalOffers = VehicleOffer::query()->active()->count();
            $this->addPaginatedUrls(self::SECTION_OFFERS, $basePath, $totalOffers, $perPage);
        }

        if (Settings::get('seo-sitemap-offers-top-level-type-listing', true)) {
            $this->addOfferTypeListingPages($basePath, $perPage);
        }

        if (Settings::get('seo-sitemap-offers-franchise-listing', false)) {
            $this->addFranchiseOfferListingPages($basePath, $perPage);
        }

        if (Settings::get('seo-sitemap-offers-franchise-type-listing', false)) {
            $this->addFranchiseTypeListingPages($perPage);
        }
    }

    private function getOfferListingBasePath(): string
    {
        $listingPath = Settings::get('seo-sitemap-offers-listing-path', '/offers');
        return '/' . trim($listingPath, '/');
    }

    private function addListingUrl(string $section, string $path): void
    {
        if ($this->isExcluded($path)) {
            return;
        }

        $this->addUrl(
            $section,
            Url::create($this->sitemapUrl($path))
                ->setChangeFrequency('daily')
                ->setPriority($this->calculatePriority($path))
        );
    }

    private function addOfferTypeListingPages(
        string $basePath,
        int $perPage
    ): void {
        OfferType::query()
            ->whereHas('offers', fn ($q) => $q->active())
            ->get()
            ->each(function (OfferType $type) use ($basePath, $perPage) {
                $typePath = $basePath . '/' . $type->slug;
                $this->addListingUrl(self::SECTION_OFFERS, $typePath);

                $typeCount = $type->offers()->active()->count();
                $this->addPaginatedUrls(self::SECTION_OFFERS, $typePath, $typeCount, $perPage);
            });
    }

    private function addFranchiseOfferListingPages(
        string $basePath,
        int $perPage
    ): void {
        Franchise::query()
            ->whereHas('offers', fn ($q) => $q->active())
            ->get()
            ->each(function (Franchise $franchise) use ($basePath, $perPage) {
                $path = '/' . $franchise->slug . $basePath;
                $this->addListingUrl(self::SECTION_OFFERS, $path);

                $count = $franchise->offers()->active()->count();
                $this->addPaginatedUrls(self::SECTION_OFFERS, $path, $count, $perPage);
            });
    }

    private function addFranchiseTypeListingPages(int $perPage): void
    {
        $franchises = Franchise::query()
            ->whereHas('offers', fn ($q) => $q->active())
            ->get()
            ->keyBy('id');

        $offerTypes = OfferType::query()
            ->whereHas('offers', fn ($q) => $q->active())
            ->get()
            ->keyBy('id');

        VehicleOffer::query()
            ->active()
            ->whereNotNull('franchise_id')
            ->whereNotNull('type_id')
            ->selectRaw('franchise_id, type_id, COUNT(*) as total')
            ->groupBy('franchise_id', 'type_id')
            ->get()
            ->each(function ($row) use ($franchises, $offerTypes, $perPage) {
                $franchise = $franchises->get($row->franchise_id);
                $type = $offerTypes->get($row->type_id);

                if (!$franchise || !$type) {
                    return;
                }

                $path = '/' . $franchise->slug . '/' . $type->slug;
                $this->addListingUrl(self::SECTION_OFFERS, $path);
                $this->addPaginatedUrls(self::SECTION_OFFERS, $path, $row->total, $perPage);
            });
    }

    /**
     * Add a CMS page (or article) to the right section based on its category.
     */
    private function addPageToSitemap(Page $page): void
    {
        $path = $page->urlPath();

        if ($this->isExcluded($path)) {
            return;
        }

        $this->addUrl(
            $this->sectionForCategory($page->category),
            Url::create($this->sitemapUrl($path))
                ->setLastModificationDate($page->updated_at)
                ->setPriority($this->calculatePriority($path))
        );
    }

    /**
     * Add a model with a URL path to a specific section.
     */
    private function addModelUrl(string $section, ModelWithUrlPath $page): void
    {
        $path = $page->urlPath();

        if ($this->isExcluded($path)) {
            return;
        }

        $this->addUrl(
            $section,
            Url::create($this->sitemapUrl($path))
                ->setLastModificationDate($page->updated_at)
                ->setPriority($this->calculatePriority($path))
        );
    }

    private function addNewCarToSitemap(NewCar $newCar): void
    {
        $path = $newCar->urlPath();

        if (
            $newCar->franchise
            && !str_starts_with(ltrim($path, '/'), $newCar->franchise->slug . '/')
            && $this->shouldPrefixWithFranchise($newCar->franchise)
        ) {
            $path = '/' . $newCar->franchise->slug . '/' . ltrim($path, '/');
        }

        if ($this->isExcluded($path)) {
            return;
        }

        $this->addUrl(
            self::SECTION_NEW_VEHICLES,
            Url::create($this->sitemapUrl($path))
                ->setLastModificationDate($newCar->updated_at)
                ->setPriority($this->calculatePriority($path))
        );
    }

    private function addOfferToSitemap(VehicleOffer $offer): void
    {
        $path = $offer->urlPath();

        if (
            $offer->franchise
            && !str_starts_with(ltrim($path, '/'), $offer->franchise->slug . '/')
            && $this->shouldPrefixWithFranchise($offer->franchise)
        ) {
            $path = '/' . $offer->franchise->slug . '/' . ltrim($path, '/');
        }

        if ($this->isExcluded($path)) {
            return;
        }

        $this->addUrl(
            self::SECTION_OFFERS,
            Url::create($this->sitemapUrl($path))
                ->setLastModificationDate($offer->updated_at)
                ->setPriority($this->calculatePriority($path))
        );
    }

    private function shouldPrefixWithFranchise(Franchise $franchise): bool
    {
        $srpType = Settings::get('seo-sitemap-srp-type');

        if ($srpType === 'path-plus-franchise') {
            return true;
        }

        return $franchise->data['sitemap-new-cars-enabled'] ?? true;
    }

    /**
     * Check if a path matches any exclude pattern.
     * Supports exact match and wildcard (*) at the end.
     */
    private function isExcluded(string $path): bool
    {
        $path = trim($path, '/');

        foreach ($this->excludePatterns as $pattern) {
            $pattern = trim($pattern, '/');

            if (str_ends_with($pattern, '*')) {
                if (str_starts_with($path, rtrim($pattern, '*'))) {
                    return true;
                }
            } elseif ($path === $pattern) {
                return true;
            }
        }

        return false;
    }

    /**
     * Build a sitemap URL from a path, applying the www prefix setting.
     */
    private function sitemapUrl(string $path): string
    {
        $url = Site::url($path);

        return match ($this->wwwPrefix) {
            'force-www' => preg_replace('#^(https?://)(?!www\.)#', '$1www.', $url),
            'force-non-www' => preg_replace('#^(https?://)www\.#', '$1', $url),
            default => $url,
        };
    }

    /**
     * Calculate priority based on URL depth.
     * Homepage and first level pages get 1.0, each additional level reduces by 0.1
     * Minimum priority is 0.1
     */
    private function calculatePriority(string $path): float
    {
        $path = trim($path, '/');

        if (empty($path) || $path === 'home') {
            return 1.0;
        }

        $depth = substr_count($path, '/') + 1;
        $priority = 1.0 - (($depth - 1) * 0.1);

        return max(0.1, $priority);
    }

    /**
     * Add paginated URLs to a section for a listing page.
     * Starts from page 2 since the base URL represents page 1.
     *
     * @param string|null $queryString A pre-existing query string (without leading "?")
     *                                 to preserve on each pagination URL, e.g. "body_type=hatchback".
     */
    private function addPaginatedUrls(
        string $section,
        string $basePath,
        int $totalItems,
        int $perPage,
        ?string $queryString = null
    ): void {
        if (!Settings::get('seo-sitemap-include-pagination', true) || $this->isExcluded($basePath)) {
            return;
        }

        $totalPages = (int) ceil($totalItems / $perPage);
        $base = $this->sitemapUrl($basePath);

        for ($page = 2; $page <= $totalPages; $page++) {
            $url = $queryString
                ? $base . '?' . $queryString . '&page=' . $page
                : $base . '?page=' . $page;
            $this->addUrl(
                $section,
                Url::create($url)
                    ->setChangeFrequency('daily')
                    ->setPriority(max(0.1, $this->calculatePriority($basePath) - 0.1))
            );
        }
    }

    /**
     * Build the list of SRP base paths based on seo-sitemap-srp-type.
     *
     * Each entry has a `path` (without leading slash) and a `query` closure that
     * applies the base constraints (condition, dealership, etc.) to a Vehicle query.
     *
     * @return array<int, array{path: string, query: Closure(Builder): Builder}>
     */
    private function resolveSrpBasePaths(): array
    {
        return match (Settings::get('seo-sitemap-srp-type')) {
            'fixed-path' => $this->fixedPathBasePaths(),
            'path-plus-franchise' => $this->franchisePlusPathBasePaths(),
            default => $this->conditionTypeBasePaths(),
        };
    }

    /**
     * @return array<int, array{path: string, query: Closure(Builder): Builder}>
     */
    private function fixedPathBasePaths(): array
    {
        $srpPath = ltrim(Settings::get('seo-sitemap-srp-path'), '/');
        $franchises = Franchise::query()->get()
            ->filter(fn (Franchise $franchise) => ($franchise->data['sitemap-srp-enabled'] ?? false));

        $entries = [[
            'path' => $srpPath,
            'query' => fn (Builder $query) => $query,
        ]];

        foreach ($franchises as $franchise) {
            $franchiseSrpPath = !empty($franchise->data['sitemap-srp-path'])
                ? ltrim($franchise->data['sitemap-srp-path'], '/')
                : $srpPath;
            $dealershipIds = $this->getFranchiseDealershipIds($franchise);
            if (empty($dealershipIds)) {
                continue;
            }
            $entries[] = [
                'path' => $franchise->slug . '/' . $franchiseSrpPath,
                'query' => fn (Builder $query) => $query->whereIn('dealership_id', $dealershipIds),
            ];
        }

        return $entries;
    }

    /**
     * @return array<int, array{path: string, query: Closure(Builder): Builder}>
     */
    private function franchisePlusPathBasePaths(): array
    {
        $srpPath = ltrim(Settings::get('seo-sitemap-srp-path'), '/');
        $entries = [[
            'path' => $srpPath,
            'query' => fn (Builder $query) => $query,
        ]];

        foreach (Franchise::query()->get() as $franchise) {
            $franchiseSrpPath = !empty($franchise->data['sitemap-srp-path'])
                ? ltrim($franchise->data['sitemap-srp-path'], '/')
                : $srpPath;
            $dealershipIds = $this->getFranchiseDealershipIds($franchise);
            if (empty($dealershipIds)) {
                continue;
            }
            $entries[] = [
                'path' => $franchise->slug . '/' . $franchiseSrpPath,
                'query' => fn (Builder $query) => $query->whereIn('dealership_id', $dealershipIds),
            ];
        }

        return $entries;
    }

    /**
     * @return array<int, array{path: string, query: Closure(Builder): Builder}>
     */
    private function conditionTypeBasePaths(): array
    {
        $conditions = [
            'used-cars' => fn (Builder $query) => $query->where('type', 'car')->used(),
            'used-vans' => fn (Builder $query) => $query->where('type', 'lcv')->used(),
            'used-motorcycles' => fn (Builder $query) => $query->where('type', 'motorcycle')->used(),
            'new-cars' => fn (Builder $query) => $query->where('type', 'car')->new(),
            'new-vans' => fn (Builder $query) => $query->where('type', 'lcv')->new(),
            'new-motorcycles' => fn (Builder $query) => $query->where('type', 'motorcycle')->new(),
        ];

        $entries = [];
        foreach ($conditions as $type => $constraint) {
            if (!$constraint(Vehicle::query()->active())->exists()) {
                continue;
            }
            $path = ($type === 'new-cars' && Settings::get('seo-sitemap-new-cars-called-stock'))
                ? 'new-stock'
                : $type;
            $entries[] = [
                'path' => $path,
                'query' => $constraint,
            ];
        }

        return $entries;
    }

    /**
     * Get dealership IDs for a franchise, respecting multi-franchise setting.
     */
    private function getFranchiseDealershipIds(Franchise $franchise): array
    {
        if (Settings::get('dealerships-multiple-franchises')) {
            return $franchise->dealerships()->pluck('dealerships.id')->toArray();
        }

        return Dealership::query()
            ->where('franchise_id', $franchise->id)
            ->pluck('id')
            ->toArray();
    }

    /**
     * Emit SRP URLs using the legacy make → model hierarchy under a base path.
     *
     * @param Closure(Builder): Builder $baseQuery
     */
    private function addLegacyHierarchyUrls(string $basePath, Closure $baseQuery): void
    {
        $perPage = (int) (Settings::get('filter-results-per-page') ?? 15);
        $queryParamTypes = new QueryParamFilterRegistry()->configured();

        if (!$this->isExcluded($basePath)) {
            $this->addUrl(
                self::SECTION_SEARCH_RESULTS,
                Url::create($this->sitemapUrl($basePath))
                    ->setChangeFrequency('daily')
                    ->setPriority($this->calculatePriority($basePath))
            );
            $this->addPaginatedUrls(
                self::SECTION_SEARCH_RESULTS,
                $basePath,
                $baseQuery(Vehicle::query()->active())->count(),
                $perPage
            );
            $this->addQueryParamFilterUrls($basePath, $baseQuery, $queryParamTypes, $perPage);
        }

        if (!Settings::get('seo-sitemap-include-srp-makes')) {
            return;
        }

        VehicleMake::query()
            ->whereHas('vehicles', fn ($query) => $baseQuery($query->active()))
            ->get()
            ->each(function (VehicleMake $make) use ($basePath, $baseQuery, $perPage, $queryParamTypes) {
                $makePath = '/' . $basePath . '/' . $make->slug;
                $makeQuery = fn (Builder $query) => $baseQuery($query)->where('make_id', $make->id);

                if (!$this->isExcluded($makePath)) {
                    $this->addUrl(
                        self::SECTION_SEARCH_RESULTS,
                        Url::create($this->sitemapUrl($makePath))
                            ->setChangeFrequency('daily')
                            ->setPriority($this->calculatePriority($makePath))
                    );
                    $this->addPaginatedUrls(
                        self::SECTION_SEARCH_RESULTS,
                        $makePath,
                        $makeQuery(Vehicle::query()->active())->count(),
                        $perPage
                    );
                    $this->addQueryParamFilterUrls($makePath, $makeQuery, $queryParamTypes, $perPage);
                }

                if (!Settings::get('seo-sitemap-include-srp-models')) {
                    return;
                }

                VehicleModel::query()
                    ->where('make_id', $make->id)
                    ->whereHas('vehicles', fn ($query) => $makeQuery($query->active()))
                    ->get()
                    ->each(function (VehicleModel $model) use (
                        $basePath,
                        $make,
                        $makeQuery,
                        $perPage,
                        $queryParamTypes
                    ) {
                        $modelPath = '/' . $basePath . '/' . $make->slug . '/' . $model->slug;
                        if ($this->isExcluded($modelPath)) {
                            return;
                        }
                        $modelQuery = fn (Builder $query) => $makeQuery($query)->where('model_id', $model->id);

                        $this->addUrl(
                            self::SECTION_SEARCH_RESULTS,
                            Url::create($this->sitemapUrl($modelPath))
                                ->setChangeFrequency('daily')
                                ->setPriority($this->calculatePriority($modelPath))
                        );
                        $this->addPaginatedUrls(
                            self::SECTION_SEARCH_RESULTS,
                            $modelPath,
                            $modelQuery(Vehicle::query()->active())->count(),
                            $perPage
                        );
                        $this->addQueryParamFilterUrls($modelPath, $modelQuery, $queryParamTypes, $perPage);
                    });
            });
    }

    /**
     * Append `?{filter_type}={slug}` URLs (and their pagination) under a legacy
     * SRP path, one per discrete value of each configured filter type.
     *
     * Combinations of multiple filters are intentionally not enumerated.
     *
     * @param Closure(Builder): Builder $baseQuery
     * @param list<string> $queryParamTypes
     */
    private function addQueryParamFilterUrls(
        string $basePath,
        Closure $baseQuery,
        array $queryParamTypes,
        int $perPage
    ): void {
        foreach ($queryParamTypes as $filterType) {
            $slugs = FilterIndex::query()
                ->where('filter_type', $filterType)
                ->pluck('slug')
                ->unique();

            foreach ($slugs as $slug) {
                $childQuery = fn (Builder $query) => $this->applyFacetConstraint(
                    $baseQuery($query),
                    $filterType,
                    $slug
                );

                $count = $childQuery(Vehicle::query()->active())->count();
                if ($count === 0) {
                    continue;
                }

                $queryString = $filterType . '=' . $slug;
                $urlPath = ltrim($basePath, '/') . '?' . $queryString;

                if ($this->isExcluded($urlPath)) {
                    continue;
                }

                $this->addUrl(
                    self::SECTION_SEARCH_RESULTS,
                    Url::create($this->sitemapUrl($basePath) . '?' . $queryString)
                        ->setChangeFrequency('daily')
                        ->setPriority(max(0.1, $this->calculatePriority($basePath) - 0.1))
                );
                $this->addPaginatedUrls(self::SECTION_SEARCH_RESULTS, $basePath, $count, $perPage, $queryString);
            }
        }
    }

    /**
     * Load active SearchFacet rows ordered by position. Returns an empty collection
     * when the search-facets feature is disabled, or when the sitemap setting that
     * opts in to facet-driven SRP URLs is off (the caller then falls back to the
     * legacy make/model hierarchy).
     */
    private function loadFacets(): Collection
    {
        if (!Feature::isEnabled('search-facets')) {
            return collect();
        }
        if (!Settings::get('seo-sitemap-include-srp-facets')) {
            return collect();
        }
        return SearchFacet::query()->orderByPosition()->get();
    }

    /**
     * Emit SRP URLs using the configured SearchFacet hierarchy.
     *
     * The base path is emitted first (always indexable), then faceted combinations
     * are enumerated recursively. Combinations with zero active vehicles are skipped.
     * URLs whose deepest facet has robots_index=false are skipped, but deeper facets
     * under that level are still enumerated in case they're indexable.
     *
     * @param Closure(Builder): Builder $baseQuery
     */
    private function addFacetedSrpUrls(string $basePath, Closure $baseQuery, Collection $facets): void
    {
        $perPage = (int) (Settings::get('filter-results-per-page') ?? 15);

        if (!$this->isExcluded($basePath)) {
            $this->addUrl(
                self::SECTION_SEARCH_RESULTS,
                Url::create($this->sitemapUrl($basePath))
                    ->setChangeFrequency('daily')
                    ->setPriority($this->calculatePriority($basePath))
            );
            $this->addPaginatedUrls(
                self::SECTION_SEARCH_RESULTS,
                $basePath,
                $baseQuery(Vehicle::query()->active())->count(),
                $perPage
            );
        }

        $this->enumerateFacetLevel($basePath, [], $baseQuery, $facets, 0, $perPage);
    }

    /**
     * Recursively enumerate facet combinations starting at $facetIndex.
     *
     * @param list<string> $slugPath Slugs already chosen for this branch.
     * @param Closure(Builder): Builder $parentQuery Constraint chain so far.
     */
    private function enumerateFacetLevel(
        string $basePath,
        array $slugPath,
        Closure $parentQuery,
        Collection $facets,
        int $facetIndex,
        int $perPage
    ): void {
        /** @var SearchFacet $facet */
        $facet = $facets->get($facetIndex);
        if ($facet === null) {
            return;
        }

        $ids_to_check = $this->activeVehicleFacetSlugs($facet->filter_type);
        $slugs = FilterIndex::query()
            ->where('filter_type', $facet->filter_type)
            ->when(
                in_array($facet->filter_type, ['make', 'model'], true),
                fn ($query) => $query->whereIn('filter_id', $ids_to_check)
            )
            ->pluck('slug')
            ->unique();

        foreach ($slugs as $slug) {
            $childQuery = fn (Builder $query) => $this->applyFacetConstraint(
                $parentQuery($query),
                $facet->filter_type,
                $slug
            );

            $count = $childQuery(Vehicle::query()->active())->count();
            if ($count === 0) {
                continue;
            }

            $childSlugPath = [...$slugPath, $slug];
            $urlPath = '/' . $basePath . '/' . implode('/', $childSlugPath);

            if ($facet->robots_index && !$this->isExcluded($urlPath)) {
                $this->addUrl(
                    self::SECTION_SEARCH_RESULTS,
                    Url::create($this->sitemapUrl($urlPath))
                        ->setChangeFrequency('daily')
                        ->setPriority($this->calculatePriority($urlPath))
                );
                $this->addPaginatedUrls(self::SECTION_SEARCH_RESULTS, $urlPath, $count, $perPage);
            }

            $this->enumerateFacetLevel($basePath, $childSlugPath, $childQuery, $facets, $facetIndex + 1, $perPage);
        }
    }

    /**
     * Apply a single facet selection (type + slug) to a Vehicle query using the
     * existing filter class for that type.
     */
    private function applyFacetConstraint(Builder $query, string $filterType, string $slug): Builder
    {
        $filterClass = config('filter.filters.' . $filterType);
        if (!$filterClass || !is_subclass_of($filterClass, IsFilter::class)) {
            return $query;
        }
        /** @var IsFilter $filter */
        $filter = new $filterClass($filterType);
        $filter->applyFilter($query, [$slug]);
        return $query;
    }

    /**
     * IDs of makes/models that have at least one active vehicle. Memoized so the
     * lookup runs once per filter type — FilterIndex can carry tens of thousands
     * of make/model entries, while vehicles typically reference only a few hundred.
     */
    private function activeVehicleFacetSlugs(string $filterType): Collection
    {
        if (isset($this->activeVehicleTaxonomyCache[$filterType])) {
            return $this->activeVehicleTaxonomyCache[$filterType];
        }

        return $this->activeVehicleTaxonomyCache[$filterType] = match ($filterType) {
            'make' => Vehicle::query()->distinct()->pluck('make_id'),
            'model' => Vehicle::query()->distinct()->pluck('model_id'),
            default => collect()
        };
    }

    /**
     * Pick the right per-section bucket for a CMS page based on its category.
     *
     * Pages with a category configured in `pages.page_types` (blog, news, events,
     * guides, …) go to their own section so split mode can emit `sitemap-blog.xml`
     * etc. Anything else (no category, or an ad-hoc category) ends up in pages.
     */
    private function sectionForCategory(?string $category): string
    {
        if ($category === null || $category === '') {
            return self::SECTION_PAGES;
        }

        return in_array($category, $this->pageTypeCategories, true)
            ? $category
            : self::SECTION_PAGES;
    }

    /**
     * Add a URL to a section, deduped across all sections.
     *
     * First-write-wins: in split mode a URL that admins authored as a CMS Page
     * at the same path as a Dealership/data-module entry should only appear in
     * the section it was first added to (typically pages, which run first).
     * Without this, split mode would emit the same URL in multiple files —
     * single mode already collapses these at render via `unique('url')`.
     */
    private function addUrl(string $section, Url $url): void
    {
        if (isset($this->emittedUrls[$url->url])) {
            return;
        }

        if (!isset($this->sections[$section])) {
            $this->sections[$section] = Sitemap::create();
        }

        $this->sections[$section]->add($url);
        $this->emittedUrls[$url->url] = true;
    }

    /**
     * Write the collected URLs to disk. In single mode everything is merged into
     * `sitemap.xml`; in split mode each non-empty section gets its own
     * `sitemap-{section}.xml` and a `sitemap-index.xml` is written referencing
     * them. The opposite mode's stale top-level file is cleaned up so search
     * engines and CDNs don't keep serving outdated content after a mode flip.
     */
    private function writeSitemaps(): void
    {
        $tenantId = tenant('id');
        $disk = Storage::disk('file-storage');

        if (Settings::get('seo-sitemap-split')) {
            $this->writeSplit($disk, $tenantId);
        } else {
            $this->writeSingle($disk, $tenantId);
        }
    }

    private function writeSingle(Filesystem $disk, string $tenantId): void
    {
        $merged = Sitemap::create();
        foreach ($this->sections as $sitemap) {
            foreach ($sitemap->getTags() as $tag) {
                $merged->add($tag);
            }
        }

        $merged->writeToDisk('file-storage', $tenantId . '/sitemap.xml', true);

        $this->deleteSplitArtifacts($disk, $tenantId);
    }

    private function writeSplit(Filesystem $disk, string $tenantId): void
    {
        $index = SitemapIndex::create();
        $writtenBasenames = [];

        foreach ($this->sections as $section => $sitemap) {
            if (empty($sitemap->getTags())) {
                continue;
            }

            $basename = 'sitemap-' . $section . '.xml';
            $sitemap->writeToDisk('file-storage', $tenantId . '/' . $basename, true);
            $writtenBasenames[] = $basename;

            $index->add(
                SitemapTag::create($disk->url($tenantId . '/' . $basename))
                    ->setLastModificationDate(now())
            );
        }

        $index->writeToDisk('file-storage', $tenantId . '/sitemap-index.xml', true);

        if ($disk->exists($tenantId . '/sitemap.xml')) {
            $disk->delete($tenantId . '/sitemap.xml');
        }

        foreach ($disk->files($tenantId) as $file) {
            $basename = basename($file);
            if (
                str_starts_with($basename, 'sitemap-')
                && $basename !== 'sitemap-index.xml'
                && !in_array($basename, $writtenBasenames, true)
            ) {
                $disk->delete($file);
            }
        }
    }

    private function deleteSplitArtifacts(Filesystem $disk, string $tenantId): void
    {
        if ($disk->exists($tenantId . '/sitemap-index.xml')) {
            $disk->delete($tenantId . '/sitemap-index.xml');
        }

        foreach ($disk->files($tenantId) as $file) {
            $basename = basename($file);
            if (str_starts_with($basename, 'sitemap-') && $basename !== 'sitemap-index.xml') {
                $disk->delete($file);
            }
        }
    }
}
