<?php

declare(strict_types=1);

namespace App\Services;

use App\Models\Artwork;
use App\Models\Category;
use App\Models\Collection;
use App\Models\CollectionEntityLink;
use App\Models\Story;
use App\Models\Tag;
use App\Models\User;
use App\Support\AvatarUrl;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection as SupportCollection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;

class CollectionLinkService
{
    public const TYPE_CREATOR = 'creator';
    public const TYPE_ARTWORK = 'artwork';
    public const TYPE_STORY = 'story';
    public const TYPE_CATEGORY = 'category';
    public const TYPE_TAG = 'tag';
    public const TYPE_CAMPAIGN = 'campaign';
    public const TYPE_EVENT = 'event';

    /**
     * @return array<int, string>
     */
    public static function supportedTypes(): array
    {
        return [
            self::TYPE_CREATOR,
            self::TYPE_ARTWORK,
            self::TYPE_STORY,
            self::TYPE_CATEGORY,
            self::TYPE_TAG,
            self::TYPE_CAMPAIGN,
            self::TYPE_EVENT,
        ];
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    public function links(Collection $collection, bool $publicOnly = false): array
    {
        $links = CollectionEntityLink::query()
            ->where('collection_id', $collection->id)
            ->orderBy('id')
            ->get();

        return $this->mapLinks($links, $publicOnly)->values()->all();
    }

    /**
     * @return array<string, array<int, array<string, mixed>>>
     */
    public function manageableOptions(Collection $collection): array
    {
        $existingIdsByType = CollectionEntityLink::query()
            ->where('collection_id', $collection->id)
            ->get()
            ->groupBy('linked_type')
            ->map(fn (SupportCollection $items): array => $items->pluck('linked_id')->map(fn ($id): int => (int) $id)->all());

        $creatorOptions = User::query()
            ->whereNotNull('username')
            ->orderByDesc('id')
            ->limit(24)
            ->get()
            ->reject(fn (User $user): bool => in_array((int) $user->id, $existingIdsByType->get(self::TYPE_CREATOR, []), true))
            ->map(fn (User $user): array => [
                'id' => (int) $user->id,
                'label' => $user->name ?: (string) $user->username,
                'description' => $user->username ? '@' . strtolower((string) $user->username) : 'Creator',
            ])
            ->values()
            ->all();

        $artworkOptions = Artwork::query()
            ->with(['user:id,username,name', 'categories.contentType:id,name'])
            ->public()
            ->latest('published_at')
            ->latest('id')
            ->limit(24)
            ->get()
            ->reject(fn (Artwork $artwork): bool => in_array((int) $artwork->id, $existingIdsByType->get(self::TYPE_ARTWORK, []), true))
            ->map(fn (Artwork $artwork): array => [
                'id' => (int) $artwork->id,
                'label' => (string) $artwork->title,
                'description' => collect([
                    $artwork->user?->username ? '@' . strtolower((string) $artwork->user->username) : null,
                    $artwork->categories->first()?->contentType?->name,
                ])->filter()->join(' • ') ?: 'Published artwork',
            ])
            ->values()
            ->all();

        $storyOptions = Story::query()
            ->with('creator:id,username,name')
            ->published()
            ->orderByDesc('published_at')
            ->limit(24)
            ->get()
            ->reject(fn (Story $story): bool => in_array((int) $story->id, $existingIdsByType->get(self::TYPE_STORY, []), true))
            ->map(fn (Story $story): array => [
                'id' => (int) $story->id,
                'label' => (string) $story->title,
                'description' => $story->creator?->username ? '@' . strtolower((string) $story->creator->username) : 'Published story',
            ])
            ->values()
            ->all();

        $categoryOptions = Category::query()
            ->with('contentType:id,slug,name')
            ->active()
            ->orderBy('sort_order')
            ->orderBy('name')
            ->limit(24)
            ->get()
            ->reject(fn (Category $category): bool => in_array((int) $category->id, $existingIdsByType->get(self::TYPE_CATEGORY, []), true))
            ->map(fn (Category $category): array => [
                'id' => (int) $category->id,
                'label' => (string) $category->name,
                'description' => $category->contentType?->name ? $category->contentType->name . ' category' : 'Category',
            ])
            ->values()
            ->all();

        $tagOptions = Tag::query()
            ->where('is_active', true)
            ->orderByDesc('usage_count')
            ->orderBy('name')
            ->limit(24)
            ->get()
            ->reject(fn (Tag $tag): bool => in_array((int) $tag->id, $existingIdsByType->get(self::TYPE_TAG, []), true))
            ->map(fn (Tag $tag): array => [
                'id' => (int) $tag->id,
                'label' => (string) $tag->name,
                'description' => '#' . strtolower((string) $tag->slug),
            ])
            ->values()
            ->all();

        $campaignOptions = $this->syntheticLinkOptions(self::TYPE_CAMPAIGN);
        $eventOptions = $this->syntheticLinkOptions(self::TYPE_EVENT);

        return [
            self::TYPE_CREATOR => $creatorOptions,
            self::TYPE_ARTWORK => $artworkOptions,
            self::TYPE_STORY => $storyOptions,
            self::TYPE_CATEGORY => $categoryOptions,
            self::TYPE_TAG => $tagOptions,
            self::TYPE_CAMPAIGN => $campaignOptions,
            self::TYPE_EVENT => $eventOptions,
        ];
    }

    /**
     * @param array<int, array<string, mixed>> $links
     */
    public function syncLinks(Collection $collection, User $actor, array $links): Collection
    {
        $normalized = collect($links)
            ->map(function ($item): ?array {
                $type = is_array($item) ? (string) ($item['linked_type'] ?? '') : '';
                $linkedId = is_array($item) ? (int) ($item['linked_id'] ?? 0) : 0;
                $relationshipType = is_array($item) ? trim((string) ($item['relationship_type'] ?? '')) : '';

                if (! in_array($type, self::supportedTypes(), true) || $linkedId <= 0) {
                    return null;
                }

                return [
                    'linked_type' => $type,
                    'linked_id' => $linkedId,
                    'relationship_type' => $relationshipType !== '' ? $relationshipType : null,
                ];
            })
            ->filter()
            ->unique(fn (array $item): string => $item['linked_type'] . ':' . $item['linked_id'])
            ->values();

        foreach (self::supportedTypes() as $type) {
            $ids = $normalized->where('linked_type', $type)->pluck('linked_id')->map(fn ($id): int => (int) $id)->all();
            if ($ids === []) {
                continue;
            }

            if ($this->isSyntheticType($type)) {
                $resolved = collect($ids)
                    ->map(fn (int $id): ?array => $this->syntheticLinkDescriptorForId($type, $id))
                    ->filter()
                    ->values();
            } else {
                $resolved = $this->resolvedEntities($type, $ids, false);
            }

            if ($resolved->count() !== count($ids)) {
                throw ValidationException::withMessages([
                    'entity_links' => 'Choose valid entities to link to this collection.',
                ]);
            }
        }

        $before = $this->links($collection, false);

        DB::transaction(function () use ($collection, $normalized): void {
            CollectionEntityLink::query()
                ->where('collection_id', $collection->id)
                ->delete();

            if ($normalized->isEmpty()) {
                return;
            }

            $now = now();

            CollectionEntityLink::query()->insert($normalized->map(fn (array $item): array => [
                'collection_id' => (int) $collection->id,
                'linked_type' => $item['linked_type'],
                'linked_id' => (int) $item['linked_id'],
                'relationship_type' => $item['relationship_type'],
                'metadata_json' => $this->isSyntheticType((string) $item['linked_type'])
                    ? json_encode($this->syntheticLinkDescriptorForId((string) $item['linked_type'], (int) $item['linked_id']), JSON_THROW_ON_ERROR)
                    : null,
                'created_at' => $now,
                'updated_at' => $now,
            ])->all());
        });

        $fresh = $collection->fresh(['user.profile', 'coverArtwork']);

        app(\App\Services\CollectionHistoryService::class)->record(
            $fresh,
            $actor,
            'entity_links_updated',
            'Collection entity links updated.',
            ['entity_links' => $before],
            ['entity_links' => $this->links($fresh, false)]
        );

        return $fresh;
    }

    /**
     * @param SupportCollection<int, CollectionEntityLink> $links
     * @return SupportCollection<int, array<string, mixed>>
     */
    private function mapLinks(SupportCollection $links, bool $publicOnly): SupportCollection
    {
        $entityMaps = collect(self::supportedTypes())
            ->reject(fn (string $type): bool => $this->isSyntheticType($type))
            ->mapWithKeys(function (string $type) use ($links, $publicOnly): array {
            $ids = $links->where('linked_type', $type)->pluck('linked_id')->map(fn ($id): int => (int) $id)->all();

            return [$type => $this->resolvedEntities($type, $ids, $publicOnly)];
            });

        return $links->map(function (CollectionEntityLink $link) use ($entityMaps): ?array {
            if ($this->isSyntheticType((string) $link->linked_type)) {
                return $this->mapSyntheticLink($link);
            }

            $entity = $entityMaps->get((string) $link->linked_type)?->get((int) $link->linked_id);

            if (! $entity instanceof Model) {
                return null;
            }

            return $this->mapLink($link, $entity);
        })->filter()->values();
    }

    /**
     * @param array<int, int> $ids
     * @return SupportCollection<int, Model>
     */
    private function resolvedEntities(string $type, array $ids, bool $publicOnly): SupportCollection
    {
        if ($ids === []) {
            return collect();
        }

        return match ($type) {
            self::TYPE_CREATOR => User::query()
                ->whereIn('id', $ids)
                ->whereNotNull('username')
                ->get()
                ->keyBy('id'),
            self::TYPE_ARTWORK => Artwork::query()
                ->with(['user:id,username,name', 'categories.contentType:id,name'])
                ->whereIn('id', $ids)
                ->when($publicOnly, fn ($query) => $query->public())
                ->get()
                ->keyBy('id'),
            self::TYPE_STORY => Story::query()
                ->with('creator:id,username,name')
                ->whereIn('id', $ids)
                ->when($publicOnly, fn ($query) => $query->published())
                ->get()
                ->keyBy('id'),
            self::TYPE_CATEGORY => Category::query()
                ->with('contentType:id,slug,name')
                ->whereIn('id', $ids)
                ->when($publicOnly, fn ($query) => $query->active())
                ->get()
                ->keyBy('id'),
            self::TYPE_TAG => Tag::query()
                ->whereIn('id', $ids)
                ->when($publicOnly, fn ($query) => $query->where('is_active', true))
                ->get()
                ->keyBy('id'),
            default => collect(),
        };
    }

    private function mapLink(CollectionEntityLink $link, Model $entity): array
    {
        return match ((string) $link->linked_type) {
            self::TYPE_CREATOR => [
                'id' => (int) $link->id,
                'linked_type' => self::TYPE_CREATOR,
                'linked_id' => (int) $entity->getKey(),
                'relationship_type' => $link->relationship_type,
                'title' => $entity->name ?: (string) $entity->username,
                'subtitle' => $entity->username ? '@' . strtolower((string) $entity->username) : 'Creator',
                'description' => $link->relationship_type ?: 'Linked creator',
                'url' => route('profile.show', ['username' => strtolower((string) $entity->username)]),
                'image_url' => AvatarUrl::forUser((int) $entity->id),
                'meta' => 'Creator',
            ],
            self::TYPE_ARTWORK => [
                'id' => (int) $link->id,
                'linked_type' => self::TYPE_ARTWORK,
                'linked_id' => (int) $entity->getKey(),
                'relationship_type' => $link->relationship_type,
                'title' => (string) $entity->title,
                'subtitle' => collect([
                    $entity->user?->username ? '@' . strtolower((string) $entity->user->username) : null,
                    $entity->categories->first()?->contentType?->name,
                ])->filter()->join(' • ') ?: 'Artwork',
                'description' => $link->relationship_type ?: 'Linked artwork',
                'url' => route('art.show', [
                    'id' => (int) $entity->id,
                    'slug' => Str::slug((string) ($entity->slug ?: $entity->title)) ?: (string) $entity->id,
                ]),
                'image_url' => $entity->thumbUrl('md') ?? $entity->thumbnail_url,
                'meta' => 'Artwork',
            ],
            self::TYPE_STORY => [
                'id' => (int) $link->id,
                'linked_type' => self::TYPE_STORY,
                'linked_id' => (int) $entity->getKey(),
                'relationship_type' => $link->relationship_type,
                'title' => (string) $entity->title,
                'subtitle' => $entity->creator?->username ? '@' . strtolower((string) $entity->creator->username) : 'Story',
                'description' => $entity->excerpt ?: ($link->relationship_type ?: 'Linked story'),
                'url' => $entity->url,
                'image_url' => $entity->cover_url,
                'meta' => 'Story',
            ],
            self::TYPE_CATEGORY => [
                'id' => (int) $link->id,
                'linked_type' => self::TYPE_CATEGORY,
                'linked_id' => (int) $entity->getKey(),
                'relationship_type' => $link->relationship_type,
                'title' => (string) $entity->name,
                'subtitle' => $entity->contentType?->name ? $entity->contentType->name . ' category' : 'Category',
                'description' => $entity->description ?: ($link->relationship_type ?: 'Linked category'),
                'url' => url($entity->url),
                'image_url' => $entity->image ? asset($entity->image) : null,
                'meta' => 'Category',
            ],
            self::TYPE_TAG => [
                'id' => (int) $link->id,
                'linked_type' => self::TYPE_TAG,
                'linked_id' => (int) $entity->getKey(),
                'relationship_type' => $link->relationship_type,
                'title' => (string) $entity->name,
                'subtitle' => '#' . strtolower((string) $entity->slug),
                'description' => $link->relationship_type ?: sprintf('Theme tag · %d uses', (int) $entity->usage_count),
                'url' => route('tags.show', ['tag' => $entity]),
                'image_url' => null,
                'meta' => 'Tag',
            ],
            default => [],
        };
    }

    /**
     * @return array<int, array{id:int,label:string,description:string}>
     */
    private function syntheticLinkOptions(string $type): array
    {
        return $this->syntheticLinkDescriptors($type)
            ->map(fn (array $item): array => [
                'id' => (int) $item['id'],
                'label' => (string) $item['label'],
                'description' => (string) $item['description'],
            ])
            ->values()
            ->all();
    }

    private function isSyntheticType(string $type): bool
    {
        return in_array($type, [self::TYPE_CAMPAIGN, self::TYPE_EVENT], true);
    }

    /**
     * @return SupportCollection<int, array<string, mixed>>
     */
    private function syntheticLinkDescriptors(string $type): SupportCollection
    {
        return match ($type) {
            self::TYPE_CAMPAIGN => Collection::query()
                ->whereNotNull('campaign_key')
                ->where('campaign_key', '!=', '')
                ->orderBy('campaign_label')
                ->orderBy('campaign_key')
                ->get(['campaign_key', 'campaign_label'])
                ->unique('campaign_key')
                ->map(function (Collection $collection): array {
                    $key = (string) $collection->campaign_key;

                    return [
                        'id' => $this->syntheticLinkId(self::TYPE_CAMPAIGN, $key),
                        'key' => $key,
                        'label' => (string) ($collection->campaign_label ?: $this->humanizeToken($key)),
                        'description' => 'Campaign landing',
                        'subtitle' => $key,
                        'url' => route('collections.campaign.show', ['campaignKey' => $key]),
                        'meta' => 'Campaign',
                    ];
                })
                ->values(),
            self::TYPE_EVENT => Collection::query()
                ->whereNotNull('event_key')
                ->where('event_key', '!=', '')
                ->orderBy('event_label')
                ->orderBy('event_key')
                ->get(['event_key', 'event_label', 'season_key'])
                ->unique('event_key')
                ->map(function (Collection $collection): array {
                    $key = (string) $collection->event_key;
                    $seasonKey = filled($collection->season_key) ? (string) $collection->season_key : null;

                    return [
                        'id' => $this->syntheticLinkId(self::TYPE_EVENT, $key),
                        'key' => $key,
                        'label' => (string) ($collection->event_label ?: $this->humanizeToken($key)),
                        'description' => $seasonKey ? 'Event context · ' . $this->humanizeToken($seasonKey) : 'Event context',
                        'subtitle' => $seasonKey ? 'Season ' . $this->humanizeToken($seasonKey) : $key,
                        'url' => null,
                        'meta' => 'Event',
                        'season_key' => $seasonKey,
                    ];
                })
                ->values(),
            default => collect(),
        };
    }

    private function syntheticLinkDescriptorForId(string $type, int $id): ?array
    {
        return $this->syntheticLinkDescriptors($type)
            ->first(fn (array $item): bool => (int) $item['id'] === $id);
    }

    private function syntheticLinkId(string $type, string $key): int
    {
        return (int) hexdec(substr(md5($type . ':' . mb_strtolower($key)), 0, 7));
    }

    private function mapSyntheticLink(CollectionEntityLink $link): ?array
    {
        $descriptor = is_array($link->metadata_json) && $link->metadata_json !== []
            ? $link->metadata_json
            : $this->syntheticLinkDescriptorForId((string) $link->linked_type, (int) $link->linked_id);

        if (! is_array($descriptor) || empty($descriptor['label'])) {
            return null;
        }

        return [
            'id' => (int) $link->id,
            'linked_type' => (string) $link->linked_type,
            'linked_id' => (int) $link->linked_id,
            'relationship_type' => $link->relationship_type,
            'title' => (string) $descriptor['label'],
            'subtitle' => $descriptor['subtitle'] ?? ((string) ($descriptor['key'] ?? '')),
            'description' => $link->relationship_type ?: (string) ($descriptor['description'] ?? 'Linked context'),
            'url' => $descriptor['url'] ?? null,
            'image_url' => null,
            'meta' => (string) ($descriptor['meta'] ?? $this->humanizeToken((string) $link->linked_type)),
            'context_key' => $descriptor['key'] ?? null,
            'season_key' => $descriptor['season_key'] ?? null,
        ];
    }

    private function humanizeToken(string $value): string
    {
        return str($value)
            ->replace(['_', '-'], ' ')
            ->title()
            ->value();
    }
}
