<?php

declare(strict_types=1);

namespace App\Services;

use App\Models\Achievement;
use App\Models\Artwork;
use App\Models\Story;
use App\Models\User;
use App\Models\UserAchievement;
use App\Notifications\AchievementUnlockedNotification;
use App\Services\Activity\UserActivityService;
use App\Services\Posts\PostAchievementService;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;

class AchievementService
{
    public function __construct(
        private readonly XPService $xp,
        private readonly PostAchievementService $achievementPosts,
    ) {}

    public function checkAchievements(User|int $user): array
    {
        $currentUser = $this->resolveUser($user);
        $unlocked = [];

        foreach ($this->unlockableDefinitions($currentUser) as $achievement) {
            if ($this->unlockAchievement($currentUser, $achievement)) {
                $unlocked[] = $achievement->slug;
            }
        }

        $this->forgetSummaryCache((int) $currentUser->id);

        return $unlocked;
    }

    public function previewUnlocks(User|int $user): array
    {
        $currentUser = $this->resolveUser($user);

        return $this->unlockableDefinitions($currentUser)
            ->pluck('slug')
            ->values()
            ->all();
    }

    public function unlockAchievement(User|int $user, Achievement|int $achievement): bool
    {
        $currentUser = $user instanceof User ? $user : User::query()->findOrFail($user);
        $currentAchievement = $achievement instanceof Achievement
            ? $achievement
            : Achievement::query()->findOrFail($achievement);

        $inserted = false;

        DB::transaction(function () use ($currentUser, $currentAchievement, &$inserted): void {
            $result = UserAchievement::query()->insertOrIgnore([
                'user_id' => (int) $currentUser->id,
                'achievement_id' => (int) $currentAchievement->id,
                'unlocked_at' => now(),
            ]);

            if ($result === 0) {
                return;
            }

            $inserted = true;
        });

        if (! $inserted) {
            return false;
        }

        if ((int) $currentAchievement->xp_reward > 0) {
            $this->xp->addXP(
                (int) $currentUser->id,
                (int) $currentAchievement->xp_reward,
                'achievement_unlocked:' . $currentAchievement->slug,
                (int) $currentAchievement->id,
                false,
            );
        }

        $currentUser->notify(new AchievementUnlockedNotification($currentAchievement));
        $this->achievementPosts->achievementUnlocked($currentUser, $currentAchievement);
        $this->forgetSummaryCache((int) $currentUser->id);

        try {
            app(UserActivityService::class)->logAchievement((int) $currentUser->id, (int) $currentAchievement->id, [
                'name' => (string) $currentAchievement->name,
            ]);
        } catch (\Throwable) {}

        return true;
    }

    public function hasAchievement(User|int $user, string $achievementSlug): bool
    {
        $userId = $user instanceof User ? (int) $user->id : $user;

        return UserAchievement::query()
            ->where('user_id', $userId)
            ->whereHas('achievement', fn ($query) => $query->where('slug', $achievementSlug))
            ->exists();
    }

    public function summary(User|int $user): array
    {
        $userId = $user instanceof User ? (int) $user->id : $user;

        return Cache::remember($this->summaryCacheKey($userId), now()->addMinutes(10), function () use ($userId): array {
            $currentUser = User::query()->with('statistics')->findOrFail($userId);
            $progress = $this->progressSnapshot($currentUser);
            $unlockedMap = UserAchievement::query()
                ->where('user_id', $userId)
                ->get()
                ->keyBy('achievement_id');

            $items = $this->definitions()->map(function (Achievement $achievement) use ($progress, $unlockedMap): array {
                $progressValue = $this->progressValue($progress, $achievement);
                /** @var UserAchievement|null $unlocked */
                $unlocked = $unlockedMap->get($achievement->id);

                return [
                    'id' => (int) $achievement->id,
                    'name' => $achievement->name,
                    'slug' => $achievement->slug,
                    'description' => $achievement->description,
                    'icon' => $achievement->icon,
                    'xp_reward' => (int) $achievement->xp_reward,
                    'type' => $achievement->type,
                    'condition_type' => $achievement->condition_type,
                    'condition_value' => (int) $achievement->condition_value,
                    'progress' => min((int) $achievement->condition_value, $progressValue),
                    'progress_percent' => $achievement->condition_value > 0
                        ? (int) round((min((int) $achievement->condition_value, $progressValue) / (int) $achievement->condition_value) * 100)
                        : 100,
                    'unlocked' => $unlocked !== null,
                    'unlocked_at' => $unlocked?->unlocked_at?->toIso8601String(),
                ];
            });

            return [
                'unlocked' => $items->where('unlocked', true)->sortByDesc('unlocked_at')->values()->all(),
                'locked' => $items->where('unlocked', false)->values()->all(),
                'recent' => $items->where('unlocked', true)->sortByDesc('unlocked_at')->take(4)->values()->all(),
                'counts' => [
                    'total' => $items->count(),
                    'unlocked' => $items->where('unlocked', true)->count(),
                    'locked' => $items->where('unlocked', false)->count(),
                ],
            ];
        });
    }

    public function definitions()
    {
        return Cache::remember('achievements:definitions', now()->addHour(), function () {
            return Achievement::query()->orderBy('type')->orderBy('condition_value')->get();
        });
    }

    public function forgetDefinitionsCache(): void
    {
        Cache::forget('achievements:definitions');
    }

    private function progressValue(array $progress, Achievement $achievement): int
    {
        return (int) ($progress[$achievement->condition_type] ?? 0);
    }

    private function resolveUser(User|int $user): User
    {
        return $user instanceof User
            ? $user->loadMissing('statistics')
            : User::query()->with('statistics')->findOrFail($user);
    }

    private function unlockableDefinitions(User $user): Collection
    {
        $progress = $this->progressSnapshot($user);
        $unlockedSlugs = $this->unlockedSlugs((int) $user->id);

        return $this->definitions()->filter(function (Achievement $achievement) use ($progress, $unlockedSlugs): bool {
            if ($this->progressValue($progress, $achievement) < (int) $achievement->condition_value) {
                return false;
            }

            return ! isset($unlockedSlugs[$achievement->slug]);
        })->values();
    }

    private function progressSnapshot(User $user): array
    {
        return [
            'upload_count' => Artwork::query()
                ->published()
                ->where('user_id', $user->id)
                ->count(),
            'likes_received' => (int) DB::table('artwork_likes as likes')
                ->join('artworks as artworks', 'artworks.id', '=', 'likes.artwork_id')
                ->where('artworks.user_id', $user->id)
                ->count(),
            'followers_count' => (int) ($user->statistics?->followers_count ?? $user->followers()->count()),
            'stories_published' => Story::query()->published()->where('creator_id', $user->id)->count(),
            'level_reached' => (int) ($user->level ?? 1),
        ];
    }

    private function unlockedSlugs(int $userId): array
    {
        return UserAchievement::query()
            ->where('user_id', $userId)
            ->join('achievements', 'achievements.id', '=', 'user_achievements.achievement_id')
            ->pluck('achievements.slug')
            ->flip()
            ->all();
    }

    private function forgetSummaryCache(int $userId): void
    {
        Cache::forget($this->summaryCacheKey($userId));
    }

    private function summaryCacheKey(int $userId): string
    {
        return 'achievements:summary:' . $userId;
    }
}
