<?php

declare(strict_types=1);

namespace App\Support\Seo;

use App\Models\Artwork;
use Illuminate\Pagination\AbstractPaginator;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;

final class SeoFactory
{
    /**
     * @param  array<string, mixed>  $meta
     */
    public function homepage(array $meta): SeoData
    {
        $description = trim((string) ($meta['description'] ?? config('seo.default_description')));

        return SeoDataBuilder::make()
            ->title((string) ($meta['title'] ?? config('seo.default_title')))
            ->description($description)
            ->keywords($meta['keywords'] ?? null)
            ->canonical((string) ($meta['canonical'] ?? url('/')))
            ->og(type: 'website', image: $meta['og_image'] ?? null)
            ->addJsonLd([
                '@context' => 'https://schema.org',
                '@type' => 'WebSite',
                'name' => (string) config('seo.site_name', 'Skinbase'),
                'url' => url('/'),
                'description' => $description,
                'potentialAction' => [
                    '@type' => 'SearchAction',
                    'target' => url('/search') . '?q={search_term_string}',
                    'query-input' => 'required name=search_term_string',
                ],
            ])
            ->build();
    }

    /**
     * @param  array<string, array<string, mixed>|null>  $thumbs
     * @param  array<int, array{name: string, url: string}>|iterable<mixed>  $breadcrumbs
     */
    public function artwork(Artwork $artwork, array $thumbs, string $canonical, iterable $breadcrumbs = []): SeoData
    {
        $authorName = html_entity_decode((string) ($artwork->user?->name ?: $artwork->user?->username ?: 'Artist'), ENT_QUOTES | ENT_HTML5, 'UTF-8');
        $title = html_entity_decode((string) ($artwork->title ?: 'Artwork'), ENT_QUOTES | ENT_HTML5, 'UTF-8');
        $description = trim(strip_tags(html_entity_decode((string) ($artwork->description ?? ''), ENT_QUOTES | ENT_HTML5, 'UTF-8')));
        $description = Str::limit($description !== '' ? $description : $title, 160, '…');
        $image = $thumbs['xl']['url'] ?? $thumbs['lg']['url'] ?? $thumbs['md']['url'] ?? null;
        $keywords = $artwork->tags->pluck('name')->filter()->unique()->values()->all();
        $publisherName = (string) config('seo.site_name', 'Skinbase');
        $publisherUrl = url('/');
        $licensePageUrl = route('terms-of-service');
        $licenseUrl = $this->clean((string) ($artwork->license_url ?? ''));
        $licenseUrl = $licenseUrl !== null ? $licenseUrl : $licensePageUrl;

        $imageWidth = $thumbs['xl']['width'] ?? $thumbs['lg']['width'] ?? null;
        $imageHeight = $thumbs['xl']['height'] ?? $thumbs['lg']['height'] ?? null;

        return SeoDataBuilder::make()
            ->title(sprintf('%s by %s — %s', $title, $authorName, config('seo.site_name', 'Skinbase')))
            ->description($description)
            ->keywords($keywords)
            ->canonical($canonical)
            ->breadcrumbs($breadcrumbs)
            ->og(type: 'article', image: $image, imageAlt: sprintf('%s by %s', $title, $authorName), imageWidth: is_int($imageWidth) ? $imageWidth : null, imageHeight: is_int($imageHeight) ? $imageHeight : null)
            ->addJsonLd(array_filter([
                '@context' => 'https://schema.org',
                '@type' => 'ImageObject',
                'name' => $title,
                'description' => $description,
                'url' => $canonical,
                'contentUrl' => $image,
                'thumbnailUrl' => $thumbs['md']['url'] ?? $image,
                'encodingFormat' => 'image/webp',
                'width' => $thumbs['xl']['width'] ?? $thumbs['lg']['width'] ?? null,
                'height' => $thumbs['xl']['height'] ?? $thumbs['lg']['height'] ?? null,
                'author' => ['@type' => 'Person', 'name' => $authorName],
                'creator' => ['@type' => 'Person', 'name' => $authorName],
                'publisher' => ['@type' => 'Organization', 'name' => $publisherName, 'url' => $publisherUrl],
                'creditText' => $authorName,
                'datePublished' => optional($artwork->published_at)->toAtomString(),
                'license' => $licenseUrl,
                'acquireLicensePage' => $licensePageUrl,
                'copyrightNotice' => $authorName,
                'keywords' => $keywords !== [] ? $keywords : null,
                'representativeOfPage' => true,
            ], fn (mixed $value): bool => $value !== null && $value !== '' && $value !== []))
            ->addJsonLd(array_filter([
                '@context' => 'https://schema.org',
                '@type' => 'CreativeWork',
                'name' => $title,
                'description' => $description,
                'url' => $canonical,
                'author' => ['@type' => 'Person', 'name' => $authorName],
                'publisher' => ['@type' => 'Organization', 'name' => $publisherName, 'url' => $publisherUrl],
                'datePublished' => optional($artwork->published_at)->toAtomString(),
                'license' => $licenseUrl,
                'keywords' => $keywords !== [] ? $keywords : null,
                'image' => $image,
            ], fn (mixed $value): bool => $value !== null && $value !== '' && $value !== []))
            ->build();
    }

    /**
     * @param  array<int, array{name: string, url: string}>|iterable<mixed>  $breadcrumbs
     * @param  array<int, string>  $keywords
     */
    public function academyLessonPage(
        string $title,
        string $description,
        string $canonical,
        ?string $image = null,
        iterable $breadcrumbs = [],
        array $keywords = [],
        ?string $publishedAt = null,
        ?string $modifiedAt = null,
        ?string $articleSection = null,
    ): SeoData {
        $publisherName = (string) config('seo.site_name', 'Skinbase');
        $publisherUrl = url('/');
        $licenseUrl = route('terms-of-service');
        $imageUrl = $this->normalizeUrl($image);

        $builder = SeoDataBuilder::make()
            ->title($title)
            ->description($description)
            ->keywords($keywords)
            ->canonical($canonical)
            ->breadcrumbs($breadcrumbs)
            ->og(type: 'article', image: $imageUrl, imageAlt: $title);

        if ($imageUrl !== null) {
            $builder->addJsonLd(array_filter([
                '@context' => 'https://schema.org',
                '@type' => 'ImageObject',
                'name' => $title,
                'description' => $description,
                'url' => $canonical,
                'contentUrl' => $imageUrl,
                'thumbnailUrl' => $imageUrl,
                'license' => $licenseUrl,
                'acquireLicensePage' => $licenseUrl,
                'creditText' => $publisherName,
                'creator' => [
                    '@type' => 'Organization',
                    'name' => $publisherName,
                    'url' => $publisherUrl,
                ],
                'publisher' => [
                    '@type' => 'Organization',
                    'name' => $publisherName,
                    'url' => $publisherUrl,
                ],
                'datePublished' => $publishedAt,
                'keywords' => $keywords !== [] ? $keywords : null,
            ], fn (mixed $value): bool => $value !== null && $value !== '' && $value !== []));
        }

        $builder->addJsonLd(array_filter([
            '@context' => 'https://schema.org',
            '@type' => 'Article',
            'headline' => $title,
            'description' => $description,
            'url' => $canonical,
            'mainEntityOfPage' => $canonical,
            'datePublished' => $publishedAt,
            'dateModified' => $modifiedAt ?? $publishedAt,
            'author' => [
                '@type' => 'Organization',
                'name' => $publisherName,
                'url' => $publisherUrl,
            ],
            'publisher' => [
                '@type' => 'Organization',
                'name' => $publisherName,
                'url' => $publisherUrl,
            ],
            'articleSection' => $articleSection,
            'keywords' => $keywords !== [] ? $keywords : null,
            'image' => $imageUrl,
        ], fn (mixed $value): bool => $value !== null && $value !== '' && $value !== []));

        return $builder->build();
    }

    /**
     * @param  iterable<int, array<string, mixed>>  $courses
     * @param  array<int, array{name: string, url: string}>|iterable<mixed>  $breadcrumbs
     */
    public function academyCourseListingPage(
        string $title,
        string $description,
        string $canonical,
        iterable $courses,
        iterable $breadcrumbs = [],
        ?string $image = null,
    ): SeoData {
        $publisherName = (string) config('seo.site_name', 'Skinbase');
        $publisherUrl = url('/');
        $imageUrl = $this->normalizeUrl($image);

        return SeoDataBuilder::make()
            ->title($title)
            ->description($description)
            ->canonical($canonical)
            ->breadcrumbs($breadcrumbs)
            ->og(type: 'website', image: $imageUrl)
            ->addJsonLd(array_filter([
                '@context' => 'https://schema.org',
                '@type' => 'CollectionPage',
                'name' => $title,
                'description' => $description,
                'url' => $canonical,
                'mainEntity' => [
                    '@type' => 'ItemList',
                    'itemListElement' => collect($courses)
                        ->values()
                        ->map(function (array $course, int $index) use ($publisherName, $publisherUrl): array {
                            $courseImage = $this->normalizeUrl((string) ($course['cover_image_url'] ?? $course['teaser_image_url'] ?? $course['cover_image'] ?? $course['teaser_image'] ?? ''));

                            return array_filter([
                                '@type' => 'ListItem',
                                'position' => $index + 1,
                                'item' => array_filter([
                                    '@type' => 'Course',
                                    'name' => $this->clean((string) ($course['title'] ?? '')),
                                    'description' => $this->clean((string) ($course['excerpt'] ?? $course['description'] ?? '')),
                                    'url' => $this->clean((string) ($course['public_url'] ?? '')),
                                    'image' => $courseImage,
                                    'provider' => [
                                        '@type' => 'Organization',
                                        'name' => $publisherName,
                                        'url' => $publisherUrl,
                                    ],
                                    'isAccessibleForFree' => $this->academyCourseAccessibleForFree($course['access_level'] ?? null),
                                    'educationalLevel' => $this->academyCourseEducationalLevel($course['difficulty'] ?? null),
                                    'timeRequired' => $this->academyDuration((int) ($course['estimated_minutes'] ?? 0)),
                                ], fn (mixed $value): bool => $value !== null && $value !== '' && $value !== []),
                            ], fn (mixed $value): bool => $value !== null && $value !== '' && $value !== []);
                        })
                        ->filter(fn (array $item): bool => data_get($item, 'item.name') !== null && data_get($item, 'item.url') !== null)
                        ->values()
                        ->all(),
                ],
            ], fn (mixed $value): bool => $value !== null && $value !== '' && $value !== []))
            ->build();
    }

    /**
     * @param  array<int, array{name: string, url: string}>|iterable<mixed>  $breadcrumbs
     * @param  array<int, string>  $keywords
     * @param  iterable<int, array<string, mixed>>  $lessons
     */
    public function academyCoursePage(
        string $title,
        string $description,
        string $canonical,
        ?string $image = null,
        iterable $breadcrumbs = [],
        array $keywords = [],
        ?string $publishedAt = null,
        ?string $modifiedAt = null,
        ?string $accessLevel = null,
        ?string $difficulty = null,
        ?int $estimatedMinutes = null,
        iterable $lessons = [],
    ): SeoData {
        $publisherName = (string) config('seo.site_name', 'Skinbase');
        $publisherUrl = url('/');
        $licenseUrl = route('terms-of-service');
        $imageUrl = $this->normalizeUrl($image);

        $builder = SeoDataBuilder::make()
            ->title($title)
            ->description($description)
            ->keywords($keywords)
            ->canonical($canonical)
            ->breadcrumbs($breadcrumbs)
            ->og(type: 'website', image: $imageUrl, imageAlt: $title);

        if ($imageUrl !== null) {
            $builder->addJsonLd(array_filter([
                '@context' => 'https://schema.org',
                '@type' => 'ImageObject',
                'name' => $title,
                'description' => $description,
                'url' => $canonical,
                'contentUrl' => $imageUrl,
                'thumbnailUrl' => $imageUrl,
                'license' => $licenseUrl,
                'acquireLicensePage' => $licenseUrl,
                'creditText' => $publisherName,
                'creator' => [
                    '@type' => 'Organization',
                    'name' => $publisherName,
                    'url' => $publisherUrl,
                ],
                'publisher' => [
                    '@type' => 'Organization',
                    'name' => $publisherName,
                    'url' => $publisherUrl,
                ],
                'representativeOfPage' => true,
                'datePublished' => $publishedAt,
                'keywords' => $keywords !== [] ? $keywords : null,
            ], fn (mixed $value): bool => $value !== null && $value !== '' && $value !== []));
        }

        $builder->addJsonLd(array_filter([
            '@context' => 'https://schema.org',
            '@type' => 'Course',
            'name' => $title,
            'description' => $description,
            'url' => $canonical,
            'image' => $imageUrl,
            'provider' => [
                '@type' => 'Organization',
                'name' => $publisherName,
                'url' => $publisherUrl,
            ],
            'isAccessibleForFree' => $this->academyCourseAccessibleForFree($accessLevel),
            'educationalLevel' => $this->academyCourseEducationalLevel($difficulty),
            'timeRequired' => $this->academyDuration($estimatedMinutes),
            'datePublished' => $publishedAt,
            'dateModified' => $modifiedAt ?? $publishedAt,
            'keywords' => $keywords !== [] ? $keywords : null,
            'hasCourseInstance' => collect($lessons)
                ->values()
                ->map(function (array $lesson, int $index): array {
                    return array_filter([
                        '@type' => 'CourseInstance',
                        'name' => $this->clean((string) ($lesson['title'] ?? '')),
                        'url' => $this->clean((string) ($lesson['course_url'] ?? '')),
                        'courseMode' => 'online',
                        'position' => $index + 1,
                    ], fn (mixed $value): bool => $value !== null && $value !== '' && $value !== []);
                })
                ->filter(fn (array $lesson): bool => ($lesson['name'] ?? null) !== null && ($lesson['url'] ?? null) !== null)
                ->values()
                ->all(),
        ], fn (mixed $value): bool => $value !== null && $value !== '' && $value !== []));

        return $builder->build();
    }

    public function profilePage(string $title, string $canonical, string $description, ?string $image = null, iterable $breadcrumbs = []): SeoData
    {
        $profileName = trim(str_replace([' Gallery on Skinbase', ' on Skinbase'], '', $title));

        return SeoDataBuilder::make()
            ->title($title)
            ->description($description)
            ->canonical($canonical)
            ->breadcrumbs($breadcrumbs)
            ->og(type: 'profile', image: $image)
            ->addJsonLd([
                '@context' => 'https://schema.org',
                '@type' => 'ProfilePage',
                'name' => $title,
                'description' => $description,
                'url' => $canonical,
                'mainEntity' => array_filter([
                    '@type' => 'Person',
                    'name' => $profileName,
                    'url' => $canonical,
                    'image' => $image,
                ], fn (mixed $value): bool => $value !== null && $value !== ''),
            ])
            ->build();
    }

    public function collectionListing(string $title, string $description, string $canonical, ?string $image = null, bool $indexable = true): SeoData
    {
        return SeoDataBuilder::make()
            ->title($title)
            ->description($description)
            ->canonical($canonical)
            ->indexable($indexable)
            ->og(type: 'website', image: $image)
            ->build();
    }

    private function normalizeUrl(?string $url): ?string
    {
        $url = $this->clean($url);

        if ($url === null) {
            return null;
        }

        if (preg_match('/^https?:\/\//i', $url) === 1) {
            return $url;
        }

        return url($url);
    }

    private function clean(?string $value): ?string
    {
        $value = trim((string) $value);

        return $value === '' ? null : $value;
    }

    private function academyCourseAccessibleForFree(mixed $accessLevel): ?bool
    {
        $access = strtolower(trim((string) $accessLevel));

        if ($access === '') {
            return null;
        }

        return $access === 'free';
    }

    private function academyCourseEducationalLevel(mixed $difficulty): ?string
    {
        $value = strtolower(trim((string) $difficulty));

        return match ($value) {
            'beginner' => 'Beginner',
            'intermediate' => 'Intermediate',
            'advanced' => 'Advanced',
            default => null,
        };
    }

    private function academyDuration(?int $minutes): ?string
    {
        $minutes = (int) ($minutes ?? 0);

        return $minutes > 0 ? sprintf('PT%dM', $minutes) : null;
    }

    public function collectionPage(string $title, string $description, string $canonical, ?string $image = null, bool $indexable = true): SeoData
    {
        return SeoDataBuilder::make()
            ->title($title)
            ->description($description)
            ->canonical($canonical)
            ->indexable($indexable)
            ->og(type: 'website', image: $image)
            ->build();
    }

    public function leaderboardPage(string $title, string $description, string $canonical): SeoData
    {
        return SeoDataBuilder::make()
            ->title($title)
            ->description($description)
            ->canonical($canonical)
            ->og(type: 'website')
            ->build();
    }

    public function simplePage(string $title, string $description, string $canonical, bool $indexable = true): SeoData
    {
        return SeoDataBuilder::make()
            ->title($title)
            ->description($description)
            ->canonical($canonical)
            ->indexable($indexable)
            ->og(type: 'website')
            ->build();
    }

    /**
     * @param  array<string, mixed>  $data
     * @return array<string, mixed>
     */
    public function fromViewData(array $data): array
    {
        if (($data['seo'] ?? null) instanceof SeoData) {
            return $data['seo']->toArray();
        }

        if (is_array($data['seo'] ?? null) && ($data['seo'] ?? []) !== []) {
            return SeoDataBuilder::fromArray($data['seo'])->build()->toArray();
        }

        $attributes = [
            'title' => $data['page_title'] ?? data_get($data, 'meta.title') ?? data_get($data, 'metaTitle'),
            'description' => $data['page_meta_description'] ?? data_get($data, 'meta.description') ?? data_get($data, 'metaDescription'),
            'keywords' => $data['page_meta_keywords'] ?? data_get($data, 'meta.keywords'),
            'canonical' => $data['page_canonical'] ?? data_get($data, 'meta.canonical') ?? url()->current(),
            'robots' => $data['page_robots'] ?? data_get($data, 'meta.robots'),
            'prev' => $data['page_rel_prev'] ?? null,
            'next' => $data['page_rel_next'] ?? null,
            'breadcrumbs' => $data['breadcrumbs'] ?? [],
            'structured_data' => Arr::wrap($data['structured_data'] ?? []),
            'faq_schema' => Arr::wrap($data['faq_schema'] ?? []),
            'og_type' => $data['seo_og_type'] ?? data_get($data, 'meta.og_type'),
            'og_title' => $data['og_title'] ?? data_get($data, 'meta.og_title'),
            'og_description' => $data['og_description'] ?? data_get($data, 'meta.og_description'),
            'og_url' => $data['og_url'] ?? data_get($data, 'meta.og_url'),
            'og_image' => $data['og_image']
                ?? $data['ogImage']
                ?? data_get($data, 'meta.og_image')
                ?? data_get($data, 'meta.ogImage')
                ?? data_get($data, 'props.hero.thumb_lg')
                ?? data_get($data, 'props.hero.thumb')
                ?? null,
            'og_image_alt' => $data['og_image_alt'] ?? null,
        ];

        $builder = SeoDataBuilder::fromArray($attributes);

        if (($data['gallery_type'] ?? null) !== null) {
            $builder->addJsonLd($this->gallerySchema($data));
        }

        return $builder->build()->toArray();
    }

    /**
     * @param  array<string, mixed>  $data
     * @return array<string, mixed>|null
     */
    private function gallerySchema(array $data): ?array
    {
        $artworks = $data['artworks'] ?? null;
        if (! $artworks instanceof AbstractPaginator && ! $artworks instanceof Collection && ! is_array($artworks)) {
            return null;
        }

        $items = $artworks instanceof AbstractPaginator
            ? collect($artworks->items())
            : collect($artworks);

        $itemListElement = $items
            ->take(12)
            ->values()
            ->map(function (mixed $artwork, int $index): ?array {
                $name = trim((string) (data_get($artwork, 'name') ?? data_get($artwork, 'title') ?? ''));
                $url = $this->galleryArtworkUrl($artwork);

                if ($name === '' && ! filled($url)) {
                    return null;
                }

                $thumbnailUrl = $this->normalizeUrl((string) (data_get($artwork, 'thumb_url') ?? ''));
                $contentUrl = $this->galleryContentUrl($artwork, $thumbnailUrl);
                $creatorName = $this->clean((string) (data_get($artwork, 'publisher.name') ?? data_get($artwork, 'uname') ?? data_get($artwork, 'username') ?? ''));
                $creatorUrl = $this->normalizeUrl((string) (data_get($artwork, 'publisher.profile_url') ?? data_get($artwork, 'profile_url') ?? ''));
                $creatorType = data_get($artwork, 'published_as_type') === 'group' || data_get($artwork, 'publisher.type') === 'group'
                    ? 'Organization'
                    : 'Person';
                $publishedAt = data_get($artwork, 'published_at');
                $publishedAt = $publishedAt instanceof \DateTimeInterface ? $publishedAt->format(DATE_ATOM) : $this->clean(is_string($publishedAt) ? $publishedAt : null);
                $width = data_get($artwork, 'width');
                $height = data_get($artwork, 'height');
                $genre = $this->clean((string) (data_get($artwork, 'content_type_name') ?? data_get($artwork, 'category_name') ?? ''));
                $imageFormat = $this->galleryImageFormat($thumbnailUrl ?? $contentUrl);

                return array_filter([
                    '@type' => 'ListItem',
                    'position' => $index + 1,
                    'url' => filled($url) ? $url : null,
                    'item' => array_filter([
                        '@type' => 'VisualArtwork',
                        '@id' => filled($url) ? $url . '#artwork' : null,
                        'name' => $name !== '' ? $name : null,
                        'url' => filled($url) ? $url : null,
                        'image' => $thumbnailUrl,
                        'thumbnailUrl' => $thumbnailUrl,
                        'contentUrl' => $contentUrl,
                        'creator' => $creatorName !== null ? array_filter([
                            '@type' => $creatorType,
                            'name' => $creatorName,
                            'url' => $creatorUrl,
                        ], fn (mixed $value): bool => $value !== null && $value !== '') : null,
                        'datePublished' => $publishedAt,
                        'genre' => $genre,
                        'encodingFormat' => $imageFormat,
                        'width' => is_numeric($width) ? (int) $width : null,
                        'height' => is_numeric($height) ? (int) $height : null,
                    ], fn (mixed $value): bool => $value !== null && $value !== '' && $value !== []),
                ], fn (mixed $value): bool => $value !== null && $value !== '');
            })
            ->filter()
            ->values()
            ->all();

        $count = $artworks instanceof AbstractPaginator ? $artworks->total() : count($itemListElement);
        $name = (string) ($data['page_title'] ?? $data['hero_title'] ?? config('seo.default_title', 'Skinbase'));
        $description = (string) ($data['page_meta_description'] ?? $data['hero_description'] ?? config('seo.default_description'));
        $canonical = (string) ($data['page_canonical'] ?? url()->current());
        $about = collect(preg_split('/\s*,\s*/', (string) ($data['page_meta_keywords'] ?? ''), -1, PREG_SPLIT_NO_EMPTY) ?: [])
            ->map(fn (string $keyword): string => trim($keyword))
            ->filter()
            ->take(5)
            ->values()
            ->all();

        return [
            '@context' => 'https://schema.org',
            '@type' => ['CollectionPage', 'ImageGallery'],
            'name' => $name,
            'headline' => (string) ($data['hero_title'] ?? $name),
            'description' => $description,
            'url' => $canonical,
            'about' => $about !== [] ? $about : null,
            'mainEntity' => [
                '@type' => 'ItemList',
                'name' => (string) ($data['hero_title'] ?? $name),
                'itemListOrder' => $this->galleryItemListOrder((string) ($data['current_sort'] ?? 'trending')),
                'numberOfItems' => $count,
                'itemListElement' => $itemListElement,
            ],
        ];
    }

    private function galleryArtworkUrl(mixed $artwork): ?string
    {
        $url = $this->normalizeUrl((string) (data_get($artwork, 'url') ?? ''));

        if ($url !== null) {
            return $url;
        }

        $id = data_get($artwork, 'id');

        if (! filled($id)) {
            return null;
        }

        $slug = $this->clean((string) (data_get($artwork, 'slug') ?? ''));

        return route('art.show', array_filter([
            'id' => $id,
            'slug' => $slug,
        ], fn (mixed $value): bool => $value !== null && $value !== ''));
    }

    private function galleryImageFormat(?string $url): ?string
    {
        $path = parse_url((string) $url, PHP_URL_PATH);
        $extension = strtolower((string) pathinfo((string) $path, PATHINFO_EXTENSION));

        return $extension !== '' ? 'image/' . $extension : null;
    }

    private function galleryContentUrl(mixed $artwork, ?string $fallbackUrl): ?string
    {
        $raw = trim((string) (data_get($artwork, 'thumb_srcset') ?? ''));

        if ($raw === '') {
            return $fallbackUrl;
        }

        $candidates = collect(explode(',', $raw))
            ->map(static function (string $candidate): array {
                $parts = preg_split('/\s+/', trim($candidate)) ?: [];
                $url = $parts[0] ?? null;
                $descriptor = $parts[1] ?? null;
                $width = is_string($descriptor) && preg_match('/^(\d+)w$/', $descriptor, $matches) === 1
                    ? (int) $matches[1]
                    : 0;

                return [
                    'url' => $url,
                    'width' => $width,
                ];
            })
            ->filter(fn (array $candidate): bool => filled($candidate['url']))
            ->sortByDesc('width')
            ->values();

        $bestUrl = $this->normalizeUrl((string) ($candidates->first()['url'] ?? ''));

        return $bestUrl ?? $fallbackUrl;
    }

    private function galleryItemListOrder(string $sort): string
    {
        return $sort === 'oldest'
            ? 'https://schema.org/ItemListOrderAscending'
            : 'https://schema.org/ItemListOrderDescending';
    }
}