<?php

namespace App\Console\Commands;

use App\Facades\Feature;
use App\Facades\Settings;
use App\Facades\Site;
use App\Filter\FilterIndex;
use Closure;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
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\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';

    private Sitemap $sitemap;

    private string $wwwPrefix;

    private array $excludePatterns = [];

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

        $this->sitemap = Sitemap::create();
        $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();

        Page::query()->active()->get()
            ->each(fn(Page $page) => $this->addToSitemap($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->addToSitemap($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->addToSitemap($page));
        }

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

        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->sitemap->add(
                    Url::create($sitemapUrl)
                        ->setPriority($this->calculatePriority($url))
                );
            });

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

        $this->sitemap->writeToDisk('file-storage', tenant('id') . '/sitemap.xml', true);
        return self::SUCCESS;
    }

    /**
     * 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('/' . $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->sitemap->add(
                            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('/' . $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($basePath);
            $totalOffers = VehicleOffer::query()->active()->count();
            $this->addPaginatedUrls($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 $path): void
    {
        if ($this->isExcluded($path)) {
            return;
        }

        $this->sitemap->add(
            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($typePath);

                $typeCount = $type->offers()->active()->count();
                $this->addPaginatedUrls($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($path);

                $count = $franchise->offers()->active()->count();
                $this->addPaginatedUrls($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($path);
                $this->addPaginatedUrls($path, $row->total, $perPage);
            });
    }

    private function addToSitemap(ModelWithUrlPath $page): void
    {
        $path = $page->urlPath();

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

        $this->sitemap->add(
            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->sitemap->add(
            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->sitemap->add(
            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, '/');

        // Homepage
        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 the sitemap for a listing page.
     * Starts from page 2 since the base URL represents page 1.
     */
    private function addPaginatedUrls(string $basePath, int $totalItems, int $perPage): void
    {
        if (!Settings::get('seo-sitemap-include-pagination', true) || $this->isExcluded($basePath)) {
            return;
        }

        $totalPages = (int) ceil($totalItems / $perPage);

        for ($page = 2; $page <= $totalPages; $page++) {
            $this->sitemap->add(
                Url::create($this->sitemapUrl($basePath) . '?page=' . $page)
                    ->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);

        if (!$this->isExcluded($basePath)) {
            $this->sitemap->add(
                Url::create($this->sitemapUrl($basePath))
                    ->setChangeFrequency('daily')
                    ->setPriority($this->calculatePriority($basePath))
            );
            $this->addPaginatedUrls($basePath, $baseQuery(Vehicle::query()->active())->count(), $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) {
                $makePath = '/' . $basePath . '/' . $make->slug;
                $makeQuery = fn(Builder $query) => $baseQuery($query)->where('make_id', $make->id);

                if (!$this->isExcluded($makePath)) {
                    $this->sitemap->add(
                        Url::create($this->sitemapUrl($makePath))
                            ->setChangeFrequency('daily')
                            ->setPriority($this->calculatePriority($makePath))
                    );
                    $this->addPaginatedUrls($makePath, $makeQuery(Vehicle::query()->active())->count(), $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) {
                        $modelPath = '/' . $basePath . '/' . $make->slug . '/' . $model->slug;
                        if ($this->isExcluded($modelPath)) {
                            return;
                        }
                        $this->sitemap->add(
                            Url::create($this->sitemapUrl($modelPath))
                                ->setChangeFrequency('daily')
                                ->setPriority($this->calculatePriority($modelPath))
                        );
                        $count = $makeQuery(Vehicle::query()->active())
                            ->where('model_id', $model->id)
                            ->count();
                        $this->addPaginatedUrls($modelPath, $count, $perPage);
                    });
            });
    }

    /**
     * Load active SearchFacet rows ordered by position, or an empty collection
     * if the search-facets feature is disabled.
     */
    private function loadFacets(): Collection
    {
        if (!Feature::isEnabled('search-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->sitemap->add(
                Url::create($this->sitemapUrl($basePath))
                    ->setChangeFrequency('daily')
                    ->setPriority($this->calculatePriority($basePath))
            );
            $this->addPaginatedUrls($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 {
        $facet = $facets->get($facetIndex);
        if ($facet === null) {
            return;
        }

        $slugs = FilterIndex::query()
            ->where('filter_type', $facet->filter_type)
            ->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->sitemap->add(
                    Url::create($this->sitemapUrl($urlPath))
                        ->setChangeFrequency('daily')
                        ->setPriority($this->calculatePriority($urlPath))
                );
                $this->addPaginatedUrls($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;
    }
}
