<?php

declare(strict_types=1);

namespace App\Services;

use App\Models\Collection;
use App\Models\User;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Support\Facades\DB;

class CollectionRecommendationService
{
    public function recommendedForUser(?User $user, int $limit = 12): EloquentCollection
    {
        $safeLimit = max(1, min($limit, 18));

        if (! $user) {
            return $this->fallbackPublicCollections($safeLimit);
        }

        $seedIds = collect()
            ->merge(DB::table('collection_saves')->where('user_id', $user->id)->pluck('collection_id'))
            ->merge(DB::table('collection_likes')->where('user_id', $user->id)->pluck('collection_id'))
            ->merge(DB::table('collection_follows')->where('user_id', $user->id)->pluck('collection_id'))
            ->map(static fn ($id) => (int) $id)
            ->unique()
            ->values();

        $followedCreatorIds = DB::table('user_followers')
            ->where('follower_id', $user->id)
            ->pluck('user_id')
            ->map(static fn ($id) => (int) $id)
            ->unique()
            ->values();

        $seedCollections = $seedIds->isEmpty()
            ? collect()
            : Collection::query()
                ->publicEligible()
                ->whereIn('id', $seedIds->all())
                ->get(['id', 'type', 'event_key', 'campaign_key', 'season_key', 'user_id']);

        $candidateQuery = Collection::query()
            ->publicEligible()
            ->with([
                'user:id,username,name',
                'coverArtwork:id,user_id,title,slug,hash,thumb_ext,published_at,is_public,is_approved,deleted_at',
            ])
            ->when($seedIds->isNotEmpty(), fn ($query) => $query->whereNotIn('id', $seedIds->all()))
            ->orderByDesc('ranking_score')
            ->orderByDesc('followers_count')
            ->orderByDesc('saves_count')
            ->orderByDesc('updated_at')
            ->limit(max(24, $safeLimit * 4));

        if ($seedCollections->isEmpty() && $followedCreatorIds->isNotEmpty()) {
            $candidateQuery->whereIn('user_id', $followedCreatorIds->all());
        }

        $candidates = $candidateQuery->get();

        if ($candidates->isEmpty()) {
            return $this->fallbackPublicCollections($safeLimit);
        }

        $candidateIds = $candidates->pluck('id')->map(static fn ($id) => (int) $id)->all();
        $creatorMap = $this->creatorMap($candidateIds);
        $tagMap = $this->tagMap($candidateIds);
        $seedTypes = $seedCollections->pluck('type')->filter()->unique()->values()->all();
        $seedCampaigns = $seedCollections->pluck('campaign_key')->filter()->unique()->values()->all();
        $seedEvents = $seedCollections->pluck('event_key')->filter()->unique()->values()->all();
        $seedSeasons = $seedCollections->pluck('season_key')->filter()->unique()->values()->all();
        $seedCreatorIds = $seedIds->isEmpty()
            ? []
            : collect($this->creatorMap($seedIds->all()))
                ->flatten()
                ->map(static fn ($id) => (int) $id)
                ->unique()
                ->values()
                ->all();
        $seedTagSlugs = $seedIds->isEmpty()
            ? []
            : $seedCollections
                ->map(fn (Collection $collection) => $this->signalTagSlugs($collection, $this->tagMap([(int) $collection->id])[(int) $collection->id] ?? []))
                ->flatten()
                ->unique()
                ->values()
                ->all();

        return new EloquentCollection($candidates
            ->map(function (Collection $candidate) use ($safeLimit, $seedTypes, $seedCampaigns, $seedEvents, $seedSeasons, $seedCreatorIds, $seedTagSlugs, $followedCreatorIds, $creatorMap, $tagMap): array {
                $candidateCreators = $creatorMap[(int) $candidate->id] ?? [];
                $candidateTags = $this->signalTagSlugs($candidate, $tagMap[(int) $candidate->id] ?? []);

                $score = 0;
                $score += in_array($candidate->type, $seedTypes, true) ? 5 : 0;
                $score += ($candidate->campaign_key && in_array($candidate->campaign_key, $seedCampaigns, true)) ? 4 : 0;
                $score += ($candidate->event_key && in_array($candidate->event_key, $seedEvents, true)) ? 4 : 0;
                $score += ($candidate->season_key && in_array($candidate->season_key, $seedSeasons, true)) ? 3 : 0;
                $score += in_array((int) $candidate->user_id, $followedCreatorIds->all(), true) ? 6 : 0;
                $score += count(array_intersect($seedCreatorIds, $candidateCreators)) * 2;
                $score += count(array_intersect($seedTagSlugs, $candidateTags));
                $score += $candidate->is_featured ? 2 : 0;
                $score += min(4, (int) floor(((int) $candidate->followers_count + (int) $candidate->saves_count) / 40));
                $score += min(3, (int) floor((float) $candidate->ranking_score / 25));

                return [
                    'score' => $score,
                    'collection' => $candidate,
                ];
            })
            ->sortByDesc(fn (array $item) => sprintf('%08d-%s', $item['score'], optional($item['collection']->updated_at)?->timestamp ?? 0))
            ->take($safeLimit)
            ->pluck('collection')
            ->values()
            ->all());
    }

    public function relatedPublicCollections(Collection $collection, int $limit = 6): EloquentCollection
    {
        $safeLimit = max(1, min($limit, 12));
        $candidates = Collection::query()
            ->publicEligible()
            ->where('id', '!=', $collection->id)
            ->with([
                'user:id,username,name',
                'coverArtwork:id,user_id,title,slug,hash,thumb_ext,published_at,is_public,is_approved,deleted_at',
            ])
            ->orderByDesc('is_featured')
            ->orderByDesc('followers_count')
            ->orderByDesc('saves_count')
            ->orderByDesc('updated_at')
            ->limit(30)
            ->get();

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

        $candidateIds = $candidates->pluck('id')->map(static fn ($id) => (int) $id)->all();
        $creatorMap = $this->creatorMap($candidateIds);
        $tagMap = $this->tagMap($candidateIds);
        $currentCreatorIds = $this->creatorMap([(int) $collection->id])[(int) $collection->id] ?? [];
        $currentTagSlugs = $this->signalTagSlugs($collection, $this->tagMap([(int) $collection->id])[(int) $collection->id] ?? []);

        return new EloquentCollection($candidates
            ->map(function (Collection $candidate) use ($collection, $creatorMap, $tagMap, $currentCreatorIds, $currentTagSlugs): array {
                $candidateCreators = $creatorMap[(int) $candidate->id] ?? [];
                $candidateTags = $this->signalTagSlugs($candidate, $tagMap[(int) $candidate->id] ?? []);

                $score = 0;
                $score += $candidate->type === $collection->type ? 4 : 0;
                $score += (int) $candidate->user_id === (int) $collection->user_id ? 3 : 0;
                $score += ($collection->event_key && $candidate->event_key === $collection->event_key) ? 4 : 0;
                $score += $candidate->is_featured ? 1 : 0;
                $score += count(array_intersect($currentCreatorIds, $candidateCreators)) * 2;
                $score += count(array_intersect($currentTagSlugs, $candidateTags));
                $score += min(2, (int) floor(((int) $candidate->saves_count + (int) $candidate->followers_count) / 25));

                return [
                    'score' => $score,
                    'collection' => $candidate,
                ];
            })
            ->sortByDesc(fn (array $item) => sprintf('%08d-%s', $item['score'], optional($item['collection']->updated_at)?->timestamp ?? 0))
            ->take($safeLimit)
            ->pluck('collection')
            ->values()
            ->all());
    }

    /**
     * @param  array<int, int> $collectionIds
     * @return array<int, array<int, int>>
     */
    private function creatorMap(array $collectionIds): array
    {
        return DB::table('collection_artwork as ca')
            ->join('artworks as a', 'a.id', '=', 'ca.artwork_id')
            ->whereIn('ca.collection_id', $collectionIds)
            ->whereNull('a.deleted_at')
            ->select('ca.collection_id', 'a.user_id')
            ->get()
            ->groupBy('collection_id')
            ->map(fn ($rows) => collect($rows)->pluck('user_id')->map(static fn ($id) => (int) $id)->unique()->values()->all())
            ->mapWithKeys(fn ($value, $key) => [(int) $key => $value])
            ->all();
    }

    /**
     * @param  array<int, int> $collectionIds
     * @return array<int, array<int, string>>
     */
    private function tagMap(array $collectionIds): array
    {
        return DB::table('collection_artwork as ca')
            ->join('artwork_tag as at', 'at.artwork_id', '=', 'ca.artwork_id')
            ->join('tags as t', 't.id', '=', 'at.tag_id')
            ->whereIn('ca.collection_id', $collectionIds)
            ->select('ca.collection_id', 't.slug')
            ->get()
            ->groupBy('collection_id')
            ->map(fn ($rows) => collect($rows)->pluck('slug')->map(static fn ($slug) => (string) $slug)->unique()->take(10)->values()->all())
            ->mapWithKeys(fn ($value, $key) => [(int) $key => $value])
            ->all();
    }

    /**
     * @param  array<int, string> $tagSlugs
     * @return array<int, string>
     */
    private function signalTagSlugs(Collection $collection, array $tagSlugs): array
    {
        if (! $collection->isSmart() || ! is_array($collection->smart_rules_json)) {
            return $tagSlugs;
        }

        $ruleTags = collect($collection->smart_rules_json['rules'] ?? [])
            ->map(fn ($rule) => is_array($rule) ? ($rule['value'] ?? null) : null)
            ->filter(fn ($value) => is_string($value) && $value !== '')
            ->map(fn (string $value) => strtolower(trim($value)))
            ->take(10)
            ->all();

        return array_values(array_unique(array_merge($tagSlugs, $ruleTags)));
    }

    private function fallbackPublicCollections(int $limit): EloquentCollection
    {
        return Collection::query()
            ->publicEligible()
            ->with([
                'user:id,username,name',
                'coverArtwork:id,user_id,title,slug,hash,thumb_ext,published_at,is_public,is_approved,deleted_at',
            ])
            ->orderByDesc('ranking_score')
            ->orderByDesc('followers_count')
            ->orderByDesc('updated_at')
            ->limit($limit)
            ->get();
    }
}
