<?php

declare(strict_types=1);

namespace App\Services\Profile;

use App\Enums\WorldRewardType;
use App\Models\Artwork;
use App\Models\GroupChallenge;
use App\Models\GroupChallengeOutcome;
use App\Models\User;
use App\Models\World;
use App\Models\WorldRelation;
use App\Models\WorldRewardGrant;
use App\Models\WorldSubmission;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;

final class WorldProfileHistoryService
{
    public function publicPayloadForUser(User $user): array
    {
        return $this->buildPayload($user, false);
    }

    public function ownerPayloadForUser(User $user): array
    {
        return $this->buildPayload($user, true);
    }

    private function buildPayload(User $user, bool $includeOwnerContext): array
    {
        $submissions = $this->submissionsForUser($user);
        $rewardGrants = $this->rewardGrantsForUser($user);
        $challengeOutcomes = $this->challengeOutcomesForUser($user);
        $challengeWorldMap = $this->challengeWorldMap($challengeOutcomes->pluck('group_challenge_id')->unique()->values());

        $entries = [];
        $hiddenPublicEntries = 0;

        foreach ($rewardGrants as $grant) {
            if (! $this->grantQualifiesForPublicHistory($grant, $submissions)) {
                $hiddenPublicEntries++;

                continue;
            }

            $this->addRecognition(
                $entries,
                $grant->world,
                $grant->reward_type->value,
                $grant->artwork,
                $grant->granted_at,
                $grant->grant_source === 'challenge' ? $this->challengeContextForWorld($grant->world) : null,
                'reward'
            );
        }

        $liveSubmissions = $submissions
            ->filter(fn (WorldSubmission $submission): bool => $this->submissionQualifiesForPublicHistory($submission));

        foreach ($liveSubmissions as $submission) {
            $recognitionKey = $submission->is_featured ? WorldRewardType::Featured->value : WorldRewardType::Participant->value;

            $this->addRecognition(
                $entries,
                $submission->world,
                $recognitionKey,
                $submission->artwork,
                $submission->featured_at ?? $submission->reviewed_at ?? $submission->created_at,
                $this->challengeContextForWorld($submission->world),
                'participation'
            );
        }

        foreach ($challengeOutcomes as $outcome) {
            $recognitionKey = $this->recognitionKeyForOutcome($outcome->outcome_type);
            if ($recognitionKey === null || ! $this->artworkIsPubliclyVisible($outcome->artwork)) {
                $hiddenPublicEntries++;

                continue;
            }

            $worlds = $challengeWorldMap->get((int) $outcome->group_challenge_id, Collection::make());

            if ($worlds->isEmpty()) {
                continue;
            }

            foreach ($worlds as $world) {
                $this->addRecognition(
                    $entries,
                    $world,
                    $recognitionKey,
                    $outcome->artwork,
                    $outcome->awarded_at ?? $outcome->created_at,
                    $this->challengeContextFromChallenge($outcome->challenge),
                    'challenge_outcome'
                );
            }
        }

        $normalizedEntries = Collection::make($entries)
            ->map(fn (array $entry): array => $this->normalizeEntry($entry))
            ->sort(function (array $left, array $right): int {
                if ($left['occurred_at'] !== $right['occurred_at']) {
                    return strcmp((string) $right['occurred_at'], (string) $left['occurred_at']);
                }

                if ($left['primary_recognition']['priority'] !== $right['primary_recognition']['priority']) {
                    return $left['primary_recognition']['priority'] <=> $right['primary_recognition']['priority'];
                }

                return strcmp((string) $left['world']['title'], (string) $right['world']['title']);
            })
            ->values();

        $yearValues = $normalizedEntries
            ->pluck('world.edition_year')
            ->filter(fn ($year): bool => is_int($year) || ctype_digit((string) $year))
            ->map(fn ($year): int => (int) $year)
            ->values();

        $worldAppearances = $normalizedEntries->count();
        $highlights = $normalizedEntries->take(3)->values();
        $mostRecent = $normalizedEntries->first();

        return [
            'summary' => [
                'available' => $normalizedEntries->isNotEmpty(),
                'world_appearances' => $worldAppearances,
                'worlds_joined' => $worldAppearances,
                'featured_appearances' => $normalizedEntries->filter(fn (array $entry): bool => in_array('featured', $entry['recognition_keys'], true))->count(),
                'winner_appearances' => $normalizedEntries->filter(fn (array $entry): bool => in_array('winner', $entry['recognition_keys'], true))->count(),
                'finalist_appearances' => $normalizedEntries->filter(fn (array $entry): bool => in_array('finalist', $entry['recognition_keys'], true))->count(),
                'spotlight_appearances' => $normalizedEntries->filter(fn (array $entry): bool => in_array('spotlight', $entry['recognition_keys'], true))->count(),
                'finalist_winner_appearances' => $normalizedEntries->filter(fn (array $entry): bool => in_array('winner', $entry['recognition_keys'], true) || in_array('finalist', $entry['recognition_keys'], true))->count(),
                'active_year_span' => $this->yearSpanPayload($yearValues),
                'most_recent_world_activity' => $mostRecent ? [
                    'world_title' => $mostRecent['world']['title'],
                    'primary_recognition' => $mostRecent['primary_recognition'],
                    'recognition_label' => $mostRecent['primary_recognition']['label'],
                    'world_url' => $mostRecent['world']['url'],
                    'occurred_at' => $mostRecent['occurred_at'],
                ] : null,
            ],
            'highlights' => $highlights->all(),
            'entries' => $normalizedEntries->all(),
            'owner_context' => $includeOwnerContext ? [
                'pending_submissions' => $submissions->where('status', WorldSubmission::STATUS_PENDING)->count(),
                'removed_or_blocked_submissions' => $submissions->filter(fn (WorldSubmission $submission): bool => in_array((string) $submission->status, [WorldSubmission::STATUS_REMOVED, WorldSubmission::STATUS_BLOCKED], true))->count(),
                'hidden_public_entries' => $hiddenPublicEntries,
            ] : null,
            'filters' => [
                'default_order' => 'recent_first',
                'groupable_by' => ['year', 'world_family', 'recognition_type'],
            ],
        ];
    }

    private function submissionsForUser(User $user): Collection
    {
        return WorldSubmission::query()
            ->with([
                'world:id,title,slug,type,recurrence_key,edition_year,linked_challenge_id,status,published_at,deleted_at',
                'world.linkedChallenge.group:id,name,slug,visibility,status',
                'artwork:id,user_id,title,slug,hash,thumb_ext,is_public,visibility,is_approved,published_at,deleted_at',
            ])
            ->where('submitted_by_user_id', (int) $user->id)
            ->whereHas('world', fn ($builder) => $builder->publiclyVisible())
            ->whereHas('artwork', fn ($builder) => $builder->where('user_id', (int) $user->id))
            ->get();
    }

    private function rewardGrantsForUser(User $user): Collection
    {
        return WorldRewardGrant::query()
            ->with([
                'world:id,title,slug,type,recurrence_key,edition_year,linked_challenge_id,status,published_at,deleted_at',
                'world.linkedChallenge.group:id,name,slug,visibility,status',
                'artwork:id,user_id,title,slug,hash,thumb_ext,is_public,visibility,is_approved,published_at,deleted_at',
                'worldSubmission:id,world_id,artwork_id,status,is_featured,featured_at,reviewed_at,created_at',
            ])
            ->where('user_id', (int) $user->id)
            ->whereHas('world', fn ($builder) => $builder->publiclyVisible())
            ->orderByDesc('granted_at')
            ->orderByDesc('id')
            ->get();
    }

    private function challengeOutcomesForUser(User $user): Collection
    {
        return GroupChallengeOutcome::query()
            ->with([
                'challenge.group:id,name,slug,visibility,status',
                'artwork:id,user_id,title,slug,hash,thumb_ext,is_public,visibility,is_approved,published_at,deleted_at',
            ])
            ->where('user_id', (int) $user->id)
            ->whereHas('artwork', fn ($builder) => $builder->where('user_id', (int) $user->id))
            ->get();
    }

    private function challengeWorldMap(Collection $challengeIds): Collection
    {
        if ($challengeIds->isEmpty()) {
            return Collection::make();
        }

        $map = Collection::make();

        World::query()
            ->with('linkedChallenge.group')
            ->publiclyVisible()
            ->whereIn('linked_challenge_id', $challengeIds->all())
            ->get()
            ->each(function (World $world) use (&$map): void {
                $challengeId = (int) ($world->linked_challenge_id ?? 0);
                if ($challengeId <= 0) {
                    return;
                }

                $items = $map->get($challengeId, Collection::make());
                $map->put($challengeId, $items->push($world)->unique('id')->values());
            });

        WorldRelation::query()
            ->with(['world.linkedChallenge.group'])
            ->where('related_type', WorldRelation::TYPE_CHALLENGE)
            ->whereIn('related_id', $challengeIds->all())
            ->whereHas('world', fn ($builder) => $builder->publiclyVisible())
            ->get()
            ->each(function (WorldRelation $relation) use (&$map): void {
                $challengeId = (int) $relation->related_id;
                $world = $relation->world;

                if (! $world) {
                    return;
                }

                $items = $map->get($challengeId, Collection::make());
                $map->put($challengeId, $items->push($world)->unique('id')->values());
            });

        return $map;
    }

    private function grantQualifiesForPublicHistory(WorldRewardGrant $grant, Collection $submissions): bool
    {
        if (! $grant->world || ! $this->artworkIsPubliclyVisible($grant->artwork)) {
            return false;
        }

        return match ($grant->reward_type) {
            WorldRewardType::Participant => $submissions->contains(fn (WorldSubmission $submission): bool => (int) $submission->world_id === (int) $grant->world_id && $this->submissionQualifiesForPublicHistory($submission)),
            WorldRewardType::Featured => $submissions->contains(fn (WorldSubmission $submission): bool => (int) $submission->world_id === (int) $grant->world_id && $this->submissionQualifiesForPublicHistory($submission) && (bool) $submission->is_featured),
            default => true,
        };
    }

    private function submissionQualifiesForPublicHistory(WorldSubmission $submission): bool
    {
        return $submission->world !== null
            && (string) $submission->status === WorldSubmission::STATUS_LIVE
            && $this->artworkIsPubliclyVisible($submission->artwork);
    }

    private function artworkIsPubliclyVisible(?Artwork $artwork): bool
    {
        if (! $artwork) {
            return false;
        }

        return $artwork->deleted_at === null
            && (bool) $artwork->is_approved
            && (bool) $artwork->is_public
            && $artwork->published_at !== null
            && $artwork->published_at->isPast()
            && in_array((string) ($artwork->visibility ?? Artwork::VISIBILITY_PUBLIC), ['', Artwork::VISIBILITY_PUBLIC], true);
    }

    private function recognitionKeyForOutcome(string $outcomeType): ?string
    {
        return match ($outcomeType) {
            GroupChallengeOutcome::TYPE_WINNER => 'winner',
            GroupChallengeOutcome::TYPE_FINALIST => 'finalist',
            GroupChallengeOutcome::TYPE_FEATURED => 'featured',
            GroupChallengeOutcome::TYPE_RUNNER_UP => 'runner_up',
            GroupChallengeOutcome::TYPE_HONORABLE_MENTION => 'honorable_mention',
            default => null,
        };
    }

    private function addRecognition(array &$entries, World $world, string $recognitionKey, ?Artwork $artwork, ?\DateTimeInterface $occurredAt, ?array $challengeContext, string $sourceType): void
    {
        $worldId = (int) $world->id;
        $timestamp = $occurredAt?->format(DATE_ATOM) ?? date(DATE_ATOM);

        if (! array_key_exists($worldId, $entries)) {
            $entries[$worldId] = [
                'world' => $world,
                'recognitions' => [],
                'occurred_at' => $timestamp,
            ];
        }

        if (! array_key_exists($recognitionKey, $entries[$worldId]['recognitions'])) {
            $entries[$worldId]['recognitions'][$recognitionKey] = [
                'recognition' => $this->recognitionPayload($recognitionKey),
                'linked_artwork' => $this->artworkPayload($artwork),
                'challenge' => $challengeContext,
                'occurred_at' => $timestamp,
                'source_types' => [$sourceType],
            ];
        } else {
            $current = $entries[$worldId]['recognitions'][$recognitionKey];

            $entries[$worldId]['recognitions'][$recognitionKey] = [
                'recognition' => $current['recognition'],
                'linked_artwork' => $current['linked_artwork'] ?? $this->artworkPayload($artwork),
                'challenge' => $current['challenge'] ?? $challengeContext,
                'occurred_at' => max((string) $current['occurred_at'], $timestamp),
                'source_types' => array_values(array_unique([...$current['source_types'], $sourceType])),
            ];
        }

        if ($timestamp > (string) $entries[$worldId]['occurred_at']) {
            $entries[$worldId]['occurred_at'] = $timestamp;
        }
    }

    private function normalizeEntry(array $entry): array
    {
        /** @var World $world */
        $world = $entry['world'];

        $recognitions = Collection::make($entry['recognitions'])
            ->sort(function (array $left, array $right): int {
                if ($left['recognition']['priority'] !== $right['recognition']['priority']) {
                    return $left['recognition']['priority'] <=> $right['recognition']['priority'];
                }

                return strcmp((string) $right['occurred_at'], (string) $left['occurred_at']);
            })
            ->values();

        $primary = $recognitions->first();
        $linkedArtwork = $primary['linked_artwork'] ?? $recognitions->pluck('linked_artwork')->first(fn ($item) => $item !== null);
        $challenge = $primary['challenge'] ?? $recognitions->pluck('challenge')->first(fn ($item) => $item !== null);

        return [
            'id' => 'world-history-' . (int) $world->id,
            'world' => [
                'id' => (int) $world->id,
                'title' => (string) $world->title,
                'slug' => (string) $world->slug,
                'url' => $world->publicUrl(),
                'type' => (string) $world->type,
                'type_label' => Str::headline((string) $world->type),
                'edition_year' => $world->edition_year ? (int) $world->edition_year : null,
                'family_key' => (string) ($world->recurrence_key ?: 'world-' . $world->id),
                'family_label' => $this->familyLabelForWorld($world),
            ],
            'primary_recognition' => $primary['recognition'],
            'recognitions' => $recognitions->map(fn (array $recognition): array => [
                ...$recognition['recognition'],
                'source_types' => $recognition['source_types'],
            ])->all(),
            'recognition_keys' => $recognitions->map(fn (array $recognition): string => (string) $recognition['recognition']['key'])->values()->all(),
            'linked_artwork' => $linkedArtwork,
            'challenge' => $challenge,
            'occurred_at' => (string) $entry['occurred_at'],
            'source_types' => $recognitions->flatMap(fn (array $recognition): array => $recognition['source_types'])->unique()->values()->all(),
        ];
    }

    private function artworkPayload(?Artwork $artwork): ?array
    {
        if (! $artwork || ! $this->artworkIsPubliclyVisible($artwork)) {
            return null;
        }

        return [
            'id' => (int) $artwork->id,
            'title' => (string) ($artwork->title ?: 'Untitled artwork'),
            'url' => route('art.show', ['id' => (int) $artwork->id, 'slug' => $artwork->slug ?: Str::slug((string) $artwork->title)]),
            'thumbnail_url' => $artwork->thumb_url,
        ];
    }

    private function challengeContextForWorld(?World $world): ?array
    {
        if (! $world || ! $world->linkedChallenge || ! $world->linkedChallenge->canBeViewedBy(null)) {
            return null;
        }

        return $this->challengeContextFromChallenge($world->linkedChallenge);
    }

    private function challengeContextFromChallenge(?GroupChallenge $challenge): ?array
    {
        if (! $challenge || ! $challenge->group || ! $challenge->canBeViewedBy(null)) {
            return null;
        }

        return [
            'id' => (int) $challenge->id,
            'title' => (string) $challenge->title,
            'url' => route('groups.challenges.show', ['group' => $challenge->group, 'challenge' => $challenge]),
            'group_name' => (string) $challenge->group->name,
        ];
    }

    private function recognitionPayload(string $recognitionKey): array
    {
        return match ($recognitionKey) {
            'winner' => ['key' => 'winner', 'label' => 'Winner', 'tone' => 'emerald', 'priority' => 0],
            'finalist' => ['key' => 'finalist', 'label' => 'Finalist', 'tone' => 'violet', 'priority' => 1],
            'featured' => ['key' => 'featured', 'label' => 'Featured', 'tone' => 'amber', 'priority' => 2],
            'spotlight' => ['key' => 'spotlight', 'label' => 'Spotlight', 'tone' => 'rose', 'priority' => 3],
            'participant' => ['key' => 'participant', 'label' => 'Participant', 'tone' => 'sky', 'priority' => 4],
            'runner_up' => ['key' => 'runner_up', 'label' => 'Runner-up', 'tone' => 'slate', 'priority' => 5],
            'honorable_mention' => ['key' => 'honorable_mention', 'label' => 'Honorable Mention', 'tone' => 'slate', 'priority' => 6],
            default => ['key' => $recognitionKey, 'label' => Str::headline(str_replace('_', ' ', $recognitionKey)), 'tone' => 'slate', 'priority' => 7],
        };
    }

    private function familyLabelForWorld(World $world): string
    {
        if ($world->recurrence_key) {
            return Str::headline(str_replace('-', ' ', (string) $world->recurrence_key));
        }

        if ($world->edition_year) {
            return trim((string) preg_replace('/\s+' . preg_quote((string) $world->edition_year, '/') . '$/', '', (string) $world->title));
        }

        return (string) $world->title;
    }

    private function yearSpanPayload(Collection $years): ?array
    {
        if ($years->isEmpty()) {
            return null;
        }

        $start = (int) $years->min();
        $end = (int) $years->max();

        return [
            'start' => $start,
            'end' => $end,
            'label' => $start === $end ? (string) $start : sprintf('%d-%d', $start, $end),
        ];
    }
}