<?php

declare(strict_types=1);

namespace App\Services\Worlds;

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 App\Notifications\WorldRewardGrantedNotification;
use App\Services\Activity\UserActivityService;
use App\Services\XPService;
use App\Support\AvatarUrl;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Validation\ValidationException;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;

final class WorldRewardService
{
    private const CHALLENGE_GRANT_SOURCE = 'challenge';

    public function __construct(
        private readonly UserActivityService $activities,
        private readonly XPService $xp,
        private readonly WorldAnalyticsService $analytics,
    ) {
    }

    public function syncAutomaticRewardsForSubmission(WorldSubmission $submission): void
    {
        $submission->loadMissing(['world', 'artwork.user.profile']);

        $world = $submission->world;
        $creator = $submission->artwork?->user;

        if (! $world || ! $creator) {
            return;
        }

        $this->syncAutomaticReward($world, $creator, WorldRewardType::Participant);
        $this->syncAutomaticReward($world, $creator, WorldRewardType::Featured);
    }

    public function grantManualReward(WorldSubmission $submission, User $editor, WorldRewardType $rewardType, ?string $note = null): WorldRewardGrant
    {
        if ($rewardType->isAutomatic()) {
            throw new \InvalidArgumentException('Automatic world rewards cannot be granted manually.');
        }

        if ((string) $submission->status !== WorldSubmission::STATUS_LIVE) {
            throw ValidationException::withMessages([
                'submission' => 'Only live world submissions can receive manual rewards.',
            ]);
        }

        $submission->loadMissing(['world', 'artwork.user.profile']);

        $world = $submission->world;
        $artwork = $submission->artwork;
        $creator = $artwork?->user;

        if (! $world || ! $artwork || ! $creator) {
            throw new \RuntimeException('Submission is missing world, artwork, or creator context.');
        }

        $grant = WorldRewardGrant::query()->firstOrNew([
            'user_id' => (int) $creator->id,
            'world_id' => (int) $world->id,
            'reward_type' => $rewardType->value,
        ]);

        $wasRecentlyCreated = ! $grant->exists;

        $grant->forceFill([
            'artwork_id' => (int) $artwork->id,
            'world_submission_id' => (int) $submission->id,
            'granted_by_user_id' => (int) $editor->id,
            'grant_source' => $rewardType->source(),
            'note' => $this->nullableText($note),
            'granted_at' => $grant->granted_at ?? now(),
        ])->save();

        $grant->loadMissing(['world', 'artwork', 'user.profile']);

        if ($wasRecentlyCreated) {
            $this->dispatchGrantSideEffects($grant);
        }

        return $grant;
    }

    public function revokeManualReward(WorldSubmission $submission, WorldRewardType $rewardType): void
    {
        if ($rewardType->isAutomatic()) {
            throw new \InvalidArgumentException('Automatic world rewards are revoked through submission state changes.');
        }

        $submission->loadMissing(['world', 'artwork.user']);

        $world = $submission->world;
        $creator = $submission->artwork?->user;

        if (! $world || ! $creator) {
            return;
        }

        WorldRewardGrant::query()
            ->where('user_id', (int) $creator->id)
            ->where('world_id', (int) $world->id)
            ->where('reward_type', $rewardType->value)
            ->where('grant_source', $rewardType->source())
            ->delete();

        $this->activities->invalidateUserFeed((int) $creator->id);
    }

    public function summaryForUser(User $user, int $limit = 12): array
    {
        $recentGrants = WorldRewardGrant::query()
            ->with(['world', 'artwork', 'user.profile'])
            ->where('user_id', (int) $user->id)
            ->orderByDesc('granted_at')
            ->orderByDesc('id')
            ->get();

        $grants = $recentGrants->sortBy([
            fn (WorldRewardGrant $grant): int => $this->sortPriority($grant->reward_type),
            fn (WorldRewardGrant $grant): int => -1 * ($grant->granted_at?->getTimestamp() ?? 0),
            fn (WorldRewardGrant $grant): int => -1 * (int) $grant->id,
        ])->values();

        return [
            'count' => $grants->count(),
            'counts' => [
                'participant' => $grants->where('reward_type', WorldRewardType::Participant)->count(),
                'featured' => $grants->where('reward_type', WorldRewardType::Featured)->count(),
                'finalist' => $grants->where('reward_type', WorldRewardType::Finalist)->count(),
                'winner' => $grants->where('reward_type', WorldRewardType::Winner)->count(),
                'spotlight' => $grants->where('reward_type', WorldRewardType::Spotlight)->count(),
            ],
            'recent' => $recentGrants->take($limit)->map(fn (WorldRewardGrant $grant): array => $this->mapGrant($grant))->all(),
            'items' => $grants->map(fn (WorldRewardGrant $grant): array => $this->mapGrant($grant))->all(),
        ];
    }

    public function rewardedContributorsForWorld(World $world, int $limit = 24): array
    {
        $baseQuery = WorldRewardGrant::query()
            ->where('world_id', (int) $world->id);

        $allGrants = (clone $baseQuery)
            ->get(['user_id', 'reward_type']);

        $grants = (clone $baseQuery)
            ->with(['user.profile', 'artwork'])
            ->orderByRaw($this->sortCaseSql())
            ->orderByDesc('granted_at')
            ->orderByDesc('id')
            ->limit($limit)
            ->get();

        return [
            'count' => $allGrants->count(),
            'creator_count' => $allGrants->pluck('user_id')->filter()->unique()->count(),
            'counts' => [
                'participant' => $allGrants->where('reward_type', WorldRewardType::Participant->value)->count(),
                'featured' => $allGrants->where('reward_type', WorldRewardType::Featured->value)->count(),
                'finalist' => $allGrants->where('reward_type', WorldRewardType::Finalist->value)->count(),
                'winner' => $allGrants->where('reward_type', WorldRewardType::Winner->value)->count(),
                'spotlight' => $allGrants->where('reward_type', WorldRewardType::Spotlight->value)->count(),
            ],
            'items' => $grants->map(fn (WorldRewardGrant $grant): array => $this->mapGrant($grant))->all(),
        ];
    }

    public function syncLinkedChallengeRewardsForWorld(World $world): void
    {
        $world->loadMissing(['worldRelations', 'linkedChallenge.group', 'linkedChallenge.outcomes']);

        if (! (bool) ($world->auto_grant_challenge_world_rewards ?? true)) {
            $this->deleteChallengeOutcomeGrantsForWorld($world);

            return;
        }

        $challengeIds = $world->worldRelations
            ->where('related_type', WorldRelation::TYPE_CHALLENGE)
            ->pluck('related_id')
            ->map(fn ($id): int => (int) $id)
            ->filter(fn (int $id): bool => $id > 0)
            ->prepend((int) ($world->linked_challenge_id ?? 0))
            ->unique()
            ->values();

        if ($challengeIds->isEmpty()) {
            $this->deleteChallengeOutcomeGrantsForWorld($world);

            return;
        }

        $challenges = GroupChallenge::query()
            ->with(['group', 'featuredArtwork.user.profile', 'outcomes.artwork.user.profile'])
            ->whereIn('id', $challengeIds->all())
            ->get()
            ->filter(fn (GroupChallenge $challenge): bool => $this->challengeCanGrantWorldOutcomeReward($challenge))
            ->values();

        $this->syncChallengeOutcomeRewardTypeForWorld($world, $challenges, WorldRewardType::Winner);
        $this->syncChallengeOutcomeRewardTypeForWorld($world, $challenges, WorldRewardType::Finalist);
    }

    private function syncChallengeOutcomeRewardTypeForWorld(World $world, Collection $challenges, WorldRewardType $rewardType): void
    {
        $artworkIds = $challenges
            ->flatMap(fn (GroupChallenge $challenge): array => $this->challengeOutcomeArtworkIds($challenge, $rewardType)->all())
            ->unique()
            ->values();

        $submissionsByArtwork = $artworkIds->isEmpty()
            ? collect()
            : WorldSubmission::query()
                ->with(['artwork.user.profile'])
                ->where('world_id', (int) $world->id)
                ->where('status', WorldSubmission::STATUS_LIVE)
                ->whereIn('artwork_id', $artworkIds->all())
                ->get()
                ->keyBy(fn (WorldSubmission $submission): int => (int) $submission->artwork_id);

        $expected = collect();

        foreach ($challenges as $challenge) {
            foreach ($this->challengeOutcomeArtworkIds($challenge, $rewardType) as $artworkId) {
                $submission = $submissionsByArtwork->get((int) $artworkId);
                $creator = $submission?->artwork?->user;

                if (! $submission || ! $creator || $expected->has((int) $creator->id)) {
                    continue;
                }

                $expected->put((int) $creator->id, [
                    'submission' => $submission,
                    'challenge' => $challenge,
                ]);
            }
        }

        $existing = WorldRewardGrant::query()
            ->where('world_id', (int) $world->id)
            ->where('reward_type', $rewardType->value)
            ->get()
            ->keyBy(fn (WorldRewardGrant $grant): int => (int) $grant->user_id);

        foreach ($expected as $userId => $payload) {
            /** @var WorldSubmission $submission */
            $submission = $payload['submission'];
            /** @var GroupChallenge $challenge */
            $challenge = $payload['challenge'];
            $current = $existing->get((int) $userId);

            if ($current && (string) $current->grant_source !== self::CHALLENGE_GRANT_SOURCE) {
                continue;
            }

            $grant = $current ?? new WorldRewardGrant();
            $wasRecentlyCreated = ! $grant->exists;

            $grant->forceFill([
                'user_id' => (int) $userId,
                'world_id' => (int) $world->id,
                'artwork_id' => (int) $submission->artwork_id,
                'world_submission_id' => (int) $submission->id,
                'granted_by_user_id' => null,
                'reward_type' => $rewardType->value,
                'grant_source' => self::CHALLENGE_GRANT_SOURCE,
                'note' => sprintf('Synced from linked challenge %s: %s.', $rewardType->label(), $challenge->title),
                'granted_at' => $grant->granted_at ?? now(),
            ])->save();

            $grant->loadMissing(['world', 'artwork', 'user.profile']);

            if ($wasRecentlyCreated) {
                $this->dispatchGrantSideEffects($grant);
            }
        }

        $expectedUserIds = $expected->keys()->map(fn ($id): int => (int) $id)->all();

        $existing
            ->filter(fn (WorldRewardGrant $grant): bool => (string) $grant->grant_source === self::CHALLENGE_GRANT_SOURCE)
            ->reject(fn (WorldRewardGrant $grant): bool => in_array((int) $grant->user_id, $expectedUserIds, true))
            ->each(function (WorldRewardGrant $grant): void {
                $grant->delete();
                $this->activities->invalidateUserFeed((int) $grant->user_id);
            });
    }

    public function syncLinkedChallengeRewardsForChallenge(GroupChallenge $challenge): void
    {
        $worldIds = WorldRelation::query()
            ->where('related_type', WorldRelation::TYPE_CHALLENGE)
            ->where('related_id', (int) $challenge->id)
            ->pluck('world_id')
            ->map(fn ($id): int => (int) $id)
            ->filter(fn (int $id): bool => $id > 0)
            ->merge(
                World::query()
                    ->where('linked_challenge_id', (int) $challenge->id)
                    ->pluck('id')
                    ->map(fn ($id): int => (int) $id)
            )
            ->unique()
            ->all();

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

        World::query()
            ->with('worldRelations')
            ->whereIn('id', $worldIds)
            ->get()
            ->each(fn (World $world): bool => tap(true, fn () => $this->syncLinkedChallengeRewardsForWorld($world)));
    }

    public function creatorRewardMapForWorld(World $world): Collection
    {
        return WorldRewardGrant::query()
            ->with(['artwork'])
            ->where('world_id', (int) $world->id)
            ->orderByRaw($this->sortCaseSql())
            ->orderByDesc('granted_at')
            ->get()
            ->groupBy('user_id')
            ->map(fn (Collection $items): array => $items->map(fn (WorldRewardGrant $grant): array => $this->mapGrant($grant, false))->all());
    }

    public function artworkRewardBadges(Artwork $artwork): array
    {
        $grants = WorldRewardGrant::query()
            ->with('world')
            ->where('artwork_id', (int) $artwork->id)
            ->orderByRaw($this->sortCaseSql())
            ->orderByDesc('granted_at')
            ->get()
            ->values();

        World::primeCanonicalEditionIds(
            $grants->pluck('world')
                ->filter()
                ->pluck('recurrence_key')
                ->all()
        );

        return $grants
            ->map(function (WorldRewardGrant $grant): array {
                $world = $grant->world;
                $rewardType = $grant->reward_type;

                return [
                    'world_id' => (int) ($world?->id ?? 0),
                    'world_title' => (string) ($world?->title ?? 'World'),
                    'world_slug' => (string) ($world?->slug ?? ''),
                    'world_url' => $world?->publicUrl(),
                    'badge_label' => $this->worldRewardLabel($world, $rewardType),
                    'status' => $rewardType->value,
                    'status_label' => $rewardType->label(),
                    'tone' => $rewardType->tone(),
                    'sort_priority' => $this->sortPriority($rewardType),
                ];
            })
            ->all();
    }

    private function syncAutomaticReward(World $world, User $creator, WorldRewardType $rewardType): void
    {
        $qualifyingSubmission = $this->qualifyingSubmission($world, $creator, $rewardType);

        $existing = WorldRewardGrant::query()
            ->where('user_id', (int) $creator->id)
            ->where('world_id', (int) $world->id)
            ->where('reward_type', $rewardType->value)
            ->first();

        if (! $qualifyingSubmission) {
            if ($rewardType === WorldRewardType::Participant) {
                return;
            }

            if ($existing && (string) $existing->grant_source === $rewardType->source()) {
                $existing->delete();
                $this->activities->invalidateUserFeed((int) $creator->id);
            }

            return;
        }

        if ($existing) {
            $existing->forceFill([
                'artwork_id' => (int) $qualifyingSubmission->artwork_id,
                'world_submission_id' => (int) $qualifyingSubmission->id,
                'grant_source' => $rewardType->source(),
            ])->save();

            return;
        }

        $grant = WorldRewardGrant::query()->create([
            'user_id' => (int) $creator->id,
            'world_id' => (int) $world->id,
            'artwork_id' => (int) $qualifyingSubmission->artwork_id,
            'world_submission_id' => (int) $qualifyingSubmission->id,
            'reward_type' => $rewardType->value,
            'grant_source' => $rewardType->source(),
            'granted_at' => now(),
        ]);

        $grant->loadMissing(['world', 'artwork', 'user.profile']);

        $this->dispatchGrantSideEffects($grant);
    }

    private function qualifyingSubmission(World $world, User $creator, WorldRewardType $rewardType): ?WorldSubmission
    {
        $query = WorldSubmission::query()
            ->with(['world', 'artwork.user.profile'])
            ->where('world_id', (int) $world->id)
            ->where('status', WorldSubmission::STATUS_LIVE)
            ->whereHas('artwork', fn (Builder $builder) => $builder->where('user_id', (int) $creator->id));

        if ($rewardType === WorldRewardType::Featured) {
            $query->where('is_featured', true)->orderByDesc('featured_at');
        } else {
            $query->orderByDesc('reviewed_at');
        }

        return $query->orderByDesc('id')->first();
    }

    private function dispatchGrantSideEffects(WorldRewardGrant $grant): void
    {
        $this->analytics->recordRewardGrant($grant);
        $grant->user?->notify(new WorldRewardGrantedNotification($grant));
        $this->activities->logWorldReward((int) $grant->user_id, (int) $grant->id, [
            'reward_type' => $grant->reward_type->value,
            'world_id' => (int) $grant->world_id,
        ]);
        $this->xp->awardWorldReward((int) $grant->user_id, $grant->reward_type, (int) $grant->world_id);
    }

    private function mapGrant(WorldRewardGrant $grant, bool $includeCreator = true): array
    {
        $grant->loadMissing(['world', 'artwork', 'user.profile']);

        $payload = [
            'id' => (int) $grant->id,
            'reward_type' => $grant->reward_type->value,
            'reward_label' => $grant->reward_type->label(),
            'badge_label' => $this->worldRewardLabel($grant->world, $grant->reward_type),
            'tone' => $grant->reward_type->tone(),
            'grant_source' => (string) $grant->grant_source,
            'note' => (string) ($grant->note ?? ''),
            'granted_at' => $grant->granted_at?->toIso8601String(),
            'world' => $grant->world ? [
                'id' => (int) $grant->world->id,
                'title' => (string) $grant->world->title,
                'slug' => (string) $grant->world->slug,
                'url' => $grant->world->publicUrl(),
                'edition_year' => $grant->world->edition_year,
            ] : null,
            'artwork' => $grant->artwork ? [
                'id' => (int) $grant->artwork->id,
                'title' => (string) ($grant->artwork->title ?: 'Untitled artwork'),
                'url' => route('art.show', ['id' => (int) $grant->artwork->id, 'slug' => $grant->artwork->slug ?: Str::slug((string) $grant->artwork->title)]),
            ] : null,
        ];

        if (! $includeCreator) {
            return $payload;
        }

        return [
            ...$payload,
            'creator' => $grant->user ? [
                'id' => (int) $grant->user->id,
                'name' => (string) ($grant->user->name ?: $grant->user->username ?: 'Creator'),
                'username' => (string) ($grant->user->username ?? ''),
                'profile_url' => $grant->user->username ? route('profile.show', ['username' => strtolower((string) $grant->user->username)]) : null,
                'avatar_url' => AvatarUrl::forUser((int) $grant->user->id, $grant->user->profile?->avatar_hash, 96),
            ] : null,
        ];
    }

    private function worldRewardLabel(?World $world, WorldRewardType $rewardType): string
    {
        return trim(($world?->title ?? 'World') . ' ' . $rewardType->label());
    }

    private function sortCaseSql(): string
    {
        return "CASE reward_type WHEN 'winner' THEN 0 WHEN 'finalist' THEN 1 WHEN 'spotlight' THEN 2 WHEN 'featured' THEN 3 ELSE 4 END";
    }

    private function challengeCanGrantWorldOutcomeReward(GroupChallenge $challenge): bool
    {
        if ((string) $challenge->status === GroupChallenge::STATUS_DRAFT) {
            return false;
        }

        return $challenge->canBeViewedBy(null);
    }

    private function challengeOutcomeArtworkIds(GroupChallenge $challenge, WorldRewardType $rewardType): Collection
    {
        $challenge->loadMissing('outcomes');

        $type = match ($rewardType) {
            WorldRewardType::Winner => GroupChallengeOutcome::TYPE_WINNER,
            WorldRewardType::Finalist => GroupChallengeOutcome::TYPE_FINALIST,
            default => null,
        };

        if ($type === null) {
            return collect();
        }

        $ids = $challenge->outcomes
            ->where('outcome_type', $type)
            ->pluck('artwork_id')
            ->map(fn ($id): int => (int) $id)
            ->filter(fn (int $id): bool => $id > 0)
            ->values();

        if ($rewardType === WorldRewardType::Winner && $ids->isEmpty() && (int) ($challenge->featured_artwork_id ?? 0) > 0) {
            return collect([(int) $challenge->featured_artwork_id]);
        }

        return $ids;
    }

    private function deleteChallengeOutcomeGrantsForWorld(World $world, ?WorldRewardType $rewardType = null): void
    {
        $query = WorldRewardGrant::query()
            ->where('world_id', (int) $world->id)
            ->where('grant_source', self::CHALLENGE_GRANT_SOURCE)
            ->when($rewardType !== null, fn ($builder) => $builder->where('reward_type', $rewardType->value));

        $query
            ->get()
            ->each(function (WorldRewardGrant $grant): void {
                $grant->delete();
                $this->activities->invalidateUserFeed((int) $grant->user_id);
            });
    }

    private function sortPriority(WorldRewardType $rewardType): int
    {
        return match ($rewardType) {
            WorldRewardType::Winner => 0,
            WorldRewardType::Finalist => 1,
            WorldRewardType::Spotlight => 2,
            WorldRewardType::Featured => 3,
            WorldRewardType::Participant => 4,
        };
    }

    private function nullableText(?string $value): ?string
    {
        $trimmed = trim((string) $value);

        return $trimmed !== '' ? $trimmed : null;
    }
}