<?php

declare(strict_types=1);

namespace App\Services;

use App\Events\Achievements\UserXpUpdated;
use App\Enums\WorldRewardType;
use App\Models\User;
use App\Models\UserXpLog;
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;

class XPService
{
    private const LEVEL_THRESHOLDS = [
        1 => 0,
        2 => 100,
        3 => 300,
        4 => 800,
        5 => 2000,
        6 => 5000,
        7 => 12000,
    ];

    private const RANKS = [
        1 => 'Newbie',
        2 => 'Explorer',
        3 => 'Contributor',
        4 => 'Creator',
        5 => 'Pro Creator',
        6 => 'Elite',
        7 => 'Legend',
    ];

    private const DAILY_CAPS = [
        'artwork_view_received' => 200,
        'comment_created' => 100,
        'story_published' => 200,
        'artwork_published' => 250,
        'follower_received' => 400,
        'artwork_like_received' => 500,
    ];

    public function addXP(
        User|int $user,
        int $amount,
        string $action,
        ?int $referenceId = null,
        bool $dispatchEvent = true,
    ): bool
    {
        if ($amount <= 0) {
            return false;
        }

        $userId = $user instanceof User ? (int) $user->id : $user;
        if ($userId <= 0) {
            return false;
        }

        $baseAction = $this->baseAction($action);
        $awardAmount = $this->applyDailyCap($userId, $amount, $baseAction);

        if ($awardAmount <= 0) {
            return false;
        }

        DB::transaction(function () use ($userId, $awardAmount, $action, $referenceId): void {
            /** @var User $lockedUser */
            $lockedUser = User::query()->lockForUpdate()->findOrFail($userId);
            $nextXp = max(0, (int) $lockedUser->xp + $awardAmount);
            $level = $this->calculateLevel($nextXp);
            $rank = $this->getRank($level);

            $lockedUser->forceFill([
                'xp' => $nextXp,
                'level' => $level,
                'rank' => $rank,
            ])->save();

            UserXpLog::query()->create([
                'user_id' => $userId,
                'action' => $action,
                'xp' => $awardAmount,
                'reference_id' => $referenceId,
                'created_at' => now(),
            ]);
        });

        $this->forgetSummaryCache($userId);

        if ($dispatchEvent) {
            event(new UserXpUpdated($userId));
        }

        return true;
    }

    public function awardArtworkPublished(int $userId, int $artworkId): bool
    {
        return $this->awardUnique($userId, 50, 'artwork_published', $artworkId);
    }

    public function awardArtworkLikeReceived(int $userId, int $artworkId, int $actorId): bool
    {
        return $this->awardUnique($userId, 5, 'artwork_like_received', $artworkId, $actorId);
    }

    public function awardFollowerReceived(int $userId, int $followerId): bool
    {
        return $this->awardUnique($userId, 20, 'follower_received', $followerId, $followerId);
    }

    public function awardStoryPublished(int $userId, int $storyId): bool
    {
        return $this->awardUnique($userId, 40, 'story_published', $storyId);
    }

    public function awardCommentCreated(int $userId, int $referenceId, string $scope = 'generic'): bool
    {
        return $this->awardUnique($userId, 5, 'comment_created:' . $scope, $referenceId);
    }

    public function awardWorldReward(int $userId, WorldRewardType $rewardType, int $worldId): bool
    {
        return $this->awardUnique($userId, $rewardType->xpReward(), 'world_reward:' . $rewardType->value, $worldId);
    }

    public function awardArtworkViewReceived(int $userId, int $artworkId, ?int $viewerId = null, ?string $ipAddress = null): bool
    {
        $viewerKey = $viewerId !== null && $viewerId > 0
            ? 'user:' . $viewerId
            : 'guest:' . sha1((string) ($ipAddress ?: 'guest'));

        $expiresAt = now()->endOfDay();
        $qualifierKey = sprintf('xp:view:qualifier:%d:%d:%s:%s', $userId, $artworkId, $viewerKey, now()->format('Ymd'));
        if (! Cache::add($qualifierKey, true, $expiresAt)) {
            return false;
        }

        $bucketKey = sprintf('xp:view:bucket:%d:%s', $userId, now()->format('Ymd'));
        Cache::add($bucketKey, 0, $expiresAt);
        $bucketCount = Cache::increment($bucketKey);

        if ($bucketCount % 10 !== 0) {
            return false;
        }

        return $this->addXP($userId, 1, 'artwork_view_received', $artworkId);
    }

    public function calculateLevel(int $xp): int
    {
        $resolvedLevel = 1;

        foreach (self::LEVEL_THRESHOLDS as $level => $threshold) {
            if ($xp >= $threshold) {
                $resolvedLevel = $level;
            }
        }

        return $resolvedLevel;
    }

    public function getRank(int $level): string
    {
        return self::RANKS[$level] ?? Arr::last(self::RANKS);
    }

    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()->findOrFail($userId, ['id', 'xp', 'level', 'rank']);
                $currentLevel = max(1, (int) $currentUser->level);
                $currentXp = max(0, (int) $currentUser->xp);
                $currentThreshold = self::LEVEL_THRESHOLDS[$currentLevel] ?? 0;
                $nextLevel = min($currentLevel + 1, array_key_last(self::LEVEL_THRESHOLDS));
                $nextLevelXp = self::LEVEL_THRESHOLDS[$nextLevel] ?? $currentXp;
                $range = max(1, $nextLevelXp - $currentThreshold);
                $progressWithinLevel = min($range, max(0, $currentXp - $currentThreshold));
                $progressPercent = $currentLevel >= array_key_last(self::LEVEL_THRESHOLDS)
                    ? 100
                    : (int) round(($progressWithinLevel / $range) * 100);

                return [
                    'xp' => $currentXp,
                    'level' => $currentLevel,
                    'rank' => (string) ($currentUser->rank ?: $this->getRank($currentLevel)),
                    'current_level_xp' => $currentThreshold,
                    'next_level_xp' => $nextLevelXp,
                    'progress_xp' => $progressWithinLevel,
                    'progress_percent' => $progressPercent,
                    'max_level' => $currentLevel >= array_key_last(self::LEVEL_THRESHOLDS),
                ];
            }
        );
    }

    public function recalculateStoredProgress(User|int $user, bool $write = true): array
    {
        $userId = $user instanceof User ? (int) $user->id : $user;

        /** @var User $currentUser */
        $currentUser = User::query()->findOrFail($userId, ['id', 'xp', 'level', 'rank']);

        $computedXp = (int) UserXpLog::query()
            ->where('user_id', $userId)
            ->sum('xp');

        $computedLevel = $this->calculateLevel($computedXp);
        $computedRank = $this->getRank($computedLevel);
        $changed = (int) $currentUser->xp !== $computedXp
            || (int) $currentUser->level !== $computedLevel
            || (string) $currentUser->rank !== $computedRank;

        if ($write && $changed) {
            $currentUser->forceFill([
                'xp' => $computedXp,
                'level' => $computedLevel,
                'rank' => $computedRank,
            ])->save();

            $this->forgetSummaryCache($userId);
        }

        return [
            'user_id' => $userId,
            'changed' => $changed,
            'previous' => [
                'xp' => (int) $currentUser->xp,
                'level' => (int) $currentUser->level,
                'rank' => (string) $currentUser->rank,
            ],
            'computed' => [
                'xp' => $computedXp,
                'level' => $computedLevel,
                'rank' => $computedRank,
            ],
        ];
    }

    private function awardUnique(int $userId, int $amount, string $action, int $referenceId, ?int $actorId = null): bool
    {
        $actionKey = $actorId !== null ? $action . ':' . $actorId : $action;

        $alreadyAwarded = UserXpLog::query()
            ->where('user_id', $userId)
            ->where('action', $actionKey)
            ->where('reference_id', $referenceId)
            ->exists();

        if ($alreadyAwarded) {
            return false;
        }

        return $this->addXP($userId, $amount, $actionKey, $referenceId);
    }

    private function applyDailyCap(int $userId, int $amount, string $baseAction): int
    {
        $cap = self::DAILY_CAPS[$baseAction] ?? null;
        if ($cap === null) {
            return $amount;
        }

        $dayStart = Carbon::now()->startOfDay();
        $awardedToday = (int) UserXpLog::query()
            ->where('user_id', $userId)
            ->where('action', 'like', $baseAction . '%')
            ->where('created_at', '>=', $dayStart)
            ->sum('xp');

        return max(0, min($amount, $cap - $awardedToday));
    }

    private function baseAction(string $action): string
    {
        return explode(':', $action, 2)[0];
    }

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

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