<?php

declare(strict_types=1);

namespace App\Support\Seo;

use Illuminate\Support\Collection;

final class SeoDataBuilder
{
    private ?string $title = null;
    private ?string $description = null;
    private ?string $keywords = null;
    private ?string $canonical = null;
    private ?string $robots = null;
    private ?string $prev = null;
    private ?string $next = null;

    /** @var list<array{name: string, url: string}> */
    private array $breadcrumbs = [];

    /** @var list<array<string, mixed>> */
    private array $jsonLd = [];

    /** @var array<string, mixed> */
    private array $og = [];

    /** @var array<string, mixed> */
    private array $twitter = [];

    public static function make(): self
    {
        return new self();
    }

    /**
     * @param  array<string, mixed>  $attributes
     */
    public static function fromArray(array $attributes): self
    {
        $builder = new self();

        if (filled($attributes['title'] ?? null)) {
            $builder->title((string) $attributes['title']);
        }
        if (filled($attributes['description'] ?? null)) {
            $builder->description((string) $attributes['description']);
        }
        if (filled($attributes['keywords'] ?? null)) {
            $builder->keywords($attributes['keywords']);
        }
        if (filled($attributes['canonical'] ?? null)) {
            $builder->canonical((string) $attributes['canonical']);
        }
        if (filled($attributes['robots'] ?? null)) {
            $builder->robots((string) $attributes['robots']);
        }
        if (filled($attributes['prev'] ?? null)) {
            $builder->prev((string) $attributes['prev']);
        }
        if (filled($attributes['next'] ?? null)) {
            $builder->next((string) $attributes['next']);
        }
        if (! empty($attributes['breadcrumbs'] ?? null)) {
            $builder->breadcrumbs($attributes['breadcrumbs']);
        }

        foreach (self::normalizeSchemaPayload($attributes['json_ld'] ?? []) as $schema) {
            $builder->addJsonLd($schema);
        }
        foreach (self::normalizeSchemaPayload($attributes['structured_data'] ?? []) as $schema) {
            $builder->addJsonLd($schema);
        }
        foreach (self::normalizeSchemaPayload($attributes['faq_schema'] ?? []) as $schema) {
            $builder->addJsonLd($schema);
        }

        $builder->og(
            type: $attributes['og_type'] ?? null,
            title: $attributes['og_title'] ?? null,
            description: $attributes['og_description'] ?? null,
            url: $attributes['og_url'] ?? null,
            image: $attributes['og_image'] ?? null,
            imageAlt: $attributes['og_image_alt'] ?? null,
        );
        $builder->twitter(
            card: $attributes['twitter_card'] ?? null,
            title: $attributes['twitter_title'] ?? null,
            description: $attributes['twitter_description'] ?? null,
            image: $attributes['twitter_image'] ?? null,
        );

        return $builder;
    }

    public function title(?string $title): self
    {
        $this->title = $this->clean($title);
        return $this;
    }

    public function description(?string $description): self
    {
        $this->description = $this->clean($description);
        return $this;
    }

    public function keywords(array|string|null $keywords): self
    {
        if (is_array($keywords)) {
            $keywords = collect($keywords)
                ->map(fn (mixed $keyword): string => trim((string) $keyword))
                ->filter()
                ->unique()
                ->implode(', ');
        }

        $this->keywords = $this->clean($keywords);
        return $this;
    }

    public function canonical(?string $canonical): self
    {
        $this->canonical = $this->absoluteUrl($canonical);
        return $this;
    }

    public function robots(?string $robots): self
    {
        $this->robots = $this->clean($robots);
        return $this;
    }

    public function indexable(bool $indexable, bool $follow = true): self
    {
        $this->robots = $indexable
            ? 'index,' . ($follow ? 'follow' : 'nofollow')
            : 'noindex,' . ($follow ? 'follow' : 'nofollow');
        return $this;
    }

    public function prev(?string $prev): self
    {
        $this->prev = $this->absoluteUrl($prev);
        return $this;
    }

    public function next(?string $next): self
    {
        $this->next = $this->absoluteUrl($next);
        return $this;
    }

    public function breadcrumbs(iterable|Collection|null $breadcrumbs): self
    {
        $this->breadcrumbs = BreadcrumbTrail::normalize($breadcrumbs);
        return $this;
    }

    /**
     * @param  array<string, mixed>|null  $schema
     */
    public function addJsonLd(?array $schema): self
    {
        if (is_array($schema) && $schema !== []) {
            $this->jsonLd[] = $schema;
        }
        return $this;
    }

    public function og(?string $type = null, ?string $title = null, ?string $description = null, ?string $url = null, ?string $image = null, ?string $imageAlt = null): self
    {
        $this->og = array_filter([
            'type' => $this->clean($type),
            'title' => $this->clean($title),
            'description' => $this->clean($description),
            'url' => $this->absoluteUrl($url),
            'image' => $this->absoluteUrl($image),
            'image_alt' => $this->clean($imageAlt),
        ], fn (mixed $value): bool => $value !== null && $value !== '');

        return $this;
    }

    public function twitter(?string $card = null, ?string $title = null, ?string $description = null, ?string $image = null): self
    {
        $this->twitter = array_filter([
            'card' => $this->clean($card),
            'title' => $this->clean($title),
            'description' => $this->clean($description),
            'image' => $this->absoluteUrl($image),
        ], fn (mixed $value): bool => $value !== null && $value !== '');

        return $this;
    }

    public function build(): SeoData
    {
        $title = $this->title ?: (string) config('seo.default_title', 'Skinbase');
        $description = $this->description ?: (string) config('seo.default_description', 'Skinbase');
        $robots = $this->robots ?: (string) config('seo.default_robots', 'index,follow');
        $canonical = $this->canonical ?: url()->current();
        $fallbackImage = $this->absoluteUrl((string) config('seo.fallback_image_path', '/gfx/skinbase_back_001.webp'));
        $image = $this->og['image'] ?? $this->twitter['image'] ?? $fallbackImage;

        $jsonLd = collect($this->jsonLd)
            ->filter(fn (mixed $schema): bool => is_array($schema) && $schema !== [])
            ->values()
            ->all();

        if ($this->breadcrumbs !== [] && ! $this->hasSchemaType($jsonLd, 'BreadcrumbList')) {
            $jsonLd[] = [
                '@context' => 'https://schema.org',
                '@type' => 'BreadcrumbList',
                'itemListElement' => collect($this->breadcrumbs)
                    ->values()
                    ->map(fn (array $crumb, int $index): array => [
                        '@type' => 'ListItem',
                        'position' => $index + 1,
                        'name' => $crumb['name'],
                        'item' => $crumb['url'],
                    ])
                    ->all(),
            ];
        }

        $keywords = config('seo.keywords_enabled', true) ? $this->keywords : null;
        $ogTitle = $this->og['title'] ?? $title;
        $ogDescription = $this->og['description'] ?? $description;
        $ogType = $this->og['type'] ?? 'website';
        $ogUrl = $this->og['url'] ?? $canonical;
        $twitterCard = $this->twitter['card'] ?? ($image !== '' ? (string) config('seo.twitter_card', 'summary_large_image') : 'summary');

        return new SeoData(array_filter([
            'title' => $title,
            'description' => $description,
            'keywords' => $keywords,
            'canonical' => $canonical,
            'robots' => $robots,
            'prev' => $this->prev,
            'next' => $this->next,
            'breadcrumbs' => $this->breadcrumbs,
            'og_type' => $ogType,
            'og_title' => $ogTitle,
            'og_description' => $ogDescription,
            'og_url' => $ogUrl,
            'og_image' => $image,
            'og_image_alt' => $this->og['image_alt'] ?? null,
            'og_site_name' => (string) config('seo.site_name', 'Skinbase'),
            'twitter_card' => $twitterCard,
            'twitter_title' => $this->twitter['title'] ?? $ogTitle,
            'twitter_description' => $this->twitter['description'] ?? $ogDescription,
            'twitter_image' => $this->twitter['image'] ?? $image,
            'json_ld' => $jsonLd,
            'is_indexable' => ! str_contains(strtolower($robots), 'noindex'),
        ], fn (mixed $value): bool => $value !== null && $value !== '' && $value !== []));
    }

    /**
     * @param  list<array<string, mixed>>  $schemas
     */
    private function hasSchemaType(array $schemas, string $type): bool
    {
        foreach ($schemas as $schema) {
            if (($schema['@type'] ?? null) === $type) {
                return true;
            }
        }

        return false;
    }

    /**
     * @return list<array<string, mixed>>
     */
    private static function normalizeSchemaPayload(mixed $payload): array
    {
        if ($payload === null || $payload === '') {
            return [];
        }

        if (is_string($payload)) {
            $decoded = json_decode($payload, true);

            return json_last_error() === JSON_ERROR_NONE
                ? self::normalizeSchemaPayload($decoded)
                : [];
        }

        if (! is_array($payload)) {
            return [];
        }

        if ($payload === []) {
            return [];
        }

        if (! array_is_list($payload)) {
            return [$payload];
        }

        return collect($payload)
            ->map(static fn (mixed $schema): array => self::normalizeSchemaPayload($schema)[0] ?? [])
            ->filter(static fn (array $schema): bool => $schema !== [])
            ->values()
            ->all();
    }

    private function clean(?string $value): ?string
    {
        $value = trim((string) $value);
        return $value === '' ? null : $value;
    }

    private function absoluteUrl(?string $url): ?string
    {
        $url = $this->clean($url);
        if ($url === null) {
            return null;
        }

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

        return url($url);
    }
}