<?php

declare(strict_types=1);

namespace App\Services;

use App\Models\Collection;
use App\Models\CollectionSave;
use App\Models\CollectionSavedNote;
use App\Models\CollectionSavedList;
use App\Models\CollectionSavedListItem;
use App\Models\User;
use Illuminate\Support\Collection as SupportCollection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;

class CollectionSavedLibraryService
{
    /**
     * @param  array<int, int>  $collectionIds
     * @return array<int, array{saved_because:?string,last_viewed_at:?string}>
     */
    public function saveMetadataFor(User $user, array $collectionIds): array
    {
        if ($collectionIds === []) {
            return [];
        }

        return CollectionSave::query()
            ->where('user_id', $user->id)
            ->whereIn('collection_id', $collectionIds)
            ->get(['collection_id', 'save_context', 'save_context_meta_json', 'last_viewed_at'])
            ->mapWithKeys(function (CollectionSave $save): array {
                return [
                    (int) $save->collection_id => [
                        'saved_because' => $this->savedBecauseLabel($save),
                        'last_viewed_at' => $save->last_viewed_at?->toIso8601String(),
                    ],
                ];
            })
            ->all();
    }

    public function recentlyRevisited(User $user, int $limit = 6): SupportCollection
    {
        $savedIds = CollectionSave::query()
            ->where('user_id', $user->id)
            ->whereNotNull('last_viewed_at')
            ->orderByDesc('last_viewed_at')
            ->limit(max(1, min($limit, 12)))
            ->pluck('collection_id')
            ->map(static fn ($id): int => (int) $id)
            ->all();

        if ($savedIds === []) {
            return collect();
        }

        $collections = Collection::query()
            ->public()
            ->with([
                'user:id,username,name',
                'coverArtwork:id,user_id,title,slug,hash,thumb_ext,published_at,is_public,is_approved,deleted_at',
            ])
            ->whereIn('id', $savedIds)
            ->get()
            ->keyBy('id');

        return collect($savedIds)
            ->map(fn (int $collectionId) => $collections->get($collectionId))
            ->filter()
            ->values();
    }

    public function listsFor(User $user): array
    {
        return CollectionSavedList::query()
            ->withCount('items')
            ->where('user_id', $user->id)
            ->orderBy('title')
            ->get()
            ->map(fn (CollectionSavedList $list) => [
                'id' => (int) $list->id,
                'title' => $list->title,
                'slug' => $list->slug,
                'items_count' => (int) $list->items_count,
            ])
            ->all();
    }

    public function findListBySlugForUser(User $user, string $slug): CollectionSavedList
    {
        return CollectionSavedList::query()
            ->withCount('items')
            ->where('user_id', $user->id)
            ->where('slug', $slug)
            ->firstOrFail();
    }

    /**
     * @param  array<int, int> $collectionIds
     * @return array<int, array<int, int>>
     */
    public function membershipsFor(User $user, array $collectionIds): array
    {
        if ($collectionIds === []) {
            return [];
        }

        return DB::table('collection_saved_list_items as items')
            ->join('collection_saved_lists as lists', 'lists.id', '=', 'items.saved_list_id')
            ->where('lists.user_id', $user->id)
            ->whereIn('items.collection_id', $collectionIds)
            ->orderBy('items.saved_list_id')
            ->get(['items.collection_id', 'items.saved_list_id'])
            ->groupBy('collection_id')
            ->map(fn ($rows) => collect($rows)->pluck('saved_list_id')->map(static fn ($id) => (int) $id)->values()->all())
            ->mapWithKeys(fn ($listIds, $collectionId) => [(int) $collectionId => $listIds])
            ->all();
    }

    /**
     * @param  array<int, int> $collectionIds
     * @return array<int, string>
     */
    public function notesFor(User $user, array $collectionIds): array
    {
        if ($collectionIds === []) {
            return [];
        }

        return CollectionSavedNote::query()
            ->where('user_id', $user->id)
            ->whereIn('collection_id', $collectionIds)
            ->pluck('note', 'collection_id')
            ->mapWithKeys(fn ($note, $collectionId) => [(int) $collectionId => (string) $note])
            ->all();
    }

    /**
     * @return array<int, int>
     */
    public function collectionIdsForList(User $user, CollectionSavedList $list): array
    {
        abort_unless((int) $list->user_id === (int) $user->id, 403);

        return CollectionSavedListItem::query()
            ->where('saved_list_id', $list->id)
            ->orderBy('order_num')
            ->pluck('collection_id')
            ->map(static fn ($id) => (int) $id)
            ->all();
    }

    public function createList(User $user, string $title): CollectionSavedList
    {
        $slug = $this->uniqueSlug($user, $title);

        return CollectionSavedList::query()->create([
            'user_id' => $user->id,
            'title' => $title,
            'slug' => $slug,
        ]);
    }

    public function addToList(User $user, CollectionSavedList $list, Collection $collection): CollectionSavedListItem
    {
        abort_unless((int) $list->user_id === (int) $user->id, 403);

        $nextOrder = (int) (CollectionSavedListItem::query()->where('saved_list_id', $list->id)->max('order_num') ?? -1) + 1;

        return CollectionSavedListItem::query()->firstOrCreate(
            [
                'saved_list_id' => $list->id,
                'collection_id' => $collection->id,
            ],
            [
                'order_num' => $nextOrder,
                'created_at' => now(),
            ]
        );
    }

    public function removeFromList(User $user, CollectionSavedList $list, Collection $collection): bool
    {
        abort_unless((int) $list->user_id === (int) $user->id, 403);

        $deleted = CollectionSavedListItem::query()
            ->where('saved_list_id', $list->id)
            ->where('collection_id', $collection->id)
            ->delete();

        if ($deleted > 0) {
            $this->normalizeOrder($list);
        }

        return $deleted > 0;
    }

    /**
     * @param  array<int, int|string> $orderedCollectionIds
     */
    public function reorderList(User $user, CollectionSavedList $list, array $orderedCollectionIds): void
    {
        abort_unless((int) $list->user_id === (int) $user->id, 403);

        $normalizedIds = collect($orderedCollectionIds)
            ->map(static fn ($id) => (int) $id)
            ->filter(static fn (int $id) => $id > 0)
            ->values();

        $currentIds = collect($this->collectionIdsForList($user, $list))->values();

        if ($normalizedIds->count() !== $currentIds->count() || $normalizedIds->diff($currentIds)->isNotEmpty() || $currentIds->diff($normalizedIds)->isNotEmpty()) {
            throw ValidationException::withMessages([
                'collection_ids' => 'The submitted saved-list order is invalid.',
            ]);
        }

        DB::transaction(function () use ($list, $normalizedIds): void {
            /** @var SupportCollection<int, int> $itemIds */
            $itemIds = CollectionSavedListItem::query()
                ->where('saved_list_id', $list->id)
                ->whereIn('collection_id', $normalizedIds->all())
                ->pluck('id', 'collection_id')
                ->mapWithKeys(static fn ($id, $collectionId) => [(int) $collectionId => (int) $id]);

            foreach ($normalizedIds as $index => $collectionId) {
                $itemId = $itemIds->get($collectionId);
                if (! $itemId) {
                    continue;
                }

                CollectionSavedListItem::query()
                    ->whereKey($itemId)
                    ->update(['order_num' => $index]);
            }
        });
    }

    public function itemsCount(CollectionSavedList $list): int
    {
        return (int) CollectionSavedListItem::query()
            ->where('saved_list_id', $list->id)
            ->count();
    }

    public function upsertNote(User $user, Collection $collection, ?string $note): ?CollectionSavedNote
    {
        $hasSavedCollection = DB::table('collection_saves')
            ->where('user_id', $user->id)
            ->where('collection_id', $collection->id)
            ->exists();

        if (! $hasSavedCollection) {
            throw ValidationException::withMessages([
                'collection' => 'You can only add notes to collections saved in your library.',
            ]);
        }

        $normalizedNote = trim((string) ($note ?? ''));

        if ($normalizedNote === '') {
            CollectionSavedNote::query()
                ->where('user_id', $user->id)
                ->where('collection_id', $collection->id)
                ->delete();

            return null;
        }

        return CollectionSavedNote::query()->updateOrCreate(
            [
                'user_id' => $user->id,
                'collection_id' => $collection->id,
            ],
            [
                'note' => $normalizedNote,
            ]
        );
    }

    private function uniqueSlug(User $user, string $title): string
    {
        $base = Str::slug(Str::limit($title, 80, '')) ?: 'saved-list';
        $slug = $base;
        $suffix = 2;

        while (CollectionSavedList::query()->where('user_id', $user->id)->where('slug', $slug)->exists()) {
            $slug = $base . '-' . $suffix;
            $suffix++;
        }

        return $slug;
    }

    private function normalizeOrder(CollectionSavedList $list): void
    {
        $itemIds = CollectionSavedListItem::query()
            ->where('saved_list_id', $list->id)
            ->orderBy('order_num')
            ->orderBy('id')
            ->pluck('id');

        foreach ($itemIds as $index => $itemId) {
            CollectionSavedListItem::query()
                ->whereKey($itemId)
                ->update(['order_num' => $index]);
        }
    }

    private function savedBecauseLabel(CollectionSave $save): ?string
    {
        $context = trim((string) ($save->save_context ?? ''));
        $meta = is_array($save->save_context_meta_json) ? $save->save_context_meta_json : [];

        return match ($context) {
            'collection_detail' => 'Saved from the collection page',
            'featured_collections' => 'Saved from featured collections',
            'featured_landing' => 'Saved from featured collections',
            'recommended_landing' => 'Saved from recommended collections',
            'trending_landing' => 'Saved from trending collections',
            'community_landing' => 'Saved from community collections',
            'editorial_landing' => 'Saved from editorial collections',
            'seasonal_landing' => 'Saved from seasonal collections',
            'collection_search' => ! empty($meta['query']) ? sprintf('Saved from search for "%s"', (string) $meta['query']) : 'Saved from collection search',
            'community_row', 'trending_row', 'editorial_row', 'seasonal_row', 'recent_row' => ! empty($meta['surface_label']) ? sprintf('Saved from %s', (string) $meta['surface_label']) : 'Saved from a collection rail',
            'program_landing' => ! empty($meta['program_label']) ? sprintf('Saved from the %s program', (string) $meta['program_label']) : (! empty($meta['program_key']) ? sprintf('Saved from the %s program', (string) $meta['program_key']) : 'Saved from a program landing'),
            'campaign_landing' => ! empty($meta['campaign_label']) ? sprintf('Saved during %s', (string) $meta['campaign_label']) : (! empty($meta['campaign_key']) ? sprintf('Saved during %s', (string) $meta['campaign_key']) : 'Saved from a campaign landing'),
            default => $context !== '' ? str_replace('_', ' ', ucfirst($context)) : null,
        };
    }
}
