<?php

/**
 * 文件功能：每日签到服务。
 *
 * 负责查询签到状态、领取每日签到奖励、防止重复领取、刷新签到身份徽章。
 */

namespace App\Services;

use App\Enums\CurrencySource;
use App\Models\DailySignIn;
use App\Models\ShopItem;
use App\Models\SignInRewardRule;
use App\Models\User;
use App\Models\UserIdentityBadge;
use App\Models\UserPurchase;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

/**
 * 类功能：封装每日签到状态查询与领取奖励的核心业务流程。
 */
class SignInService
{
    /**
     * 构造每日签到服务。
     */
    public function __construct(
        private readonly UserCurrencyService $currencyService,
    ) {}

    /**
     * 查询用户当前签到状态与今日可领取预览。
     *
     * @return array{
     *     signed_today: bool,
     *     can_claim: bool,
     *     current_streak_days: int,
     *     claimable_streak_days: int,
     *     today_sign_in: DailySignIn|null,
     *     latest_sign_in: DailySignIn|null,
     *     matched_rule: SignInRewardRule|null,
     *     current_identity: UserIdentityBadge|null
     * }
     */
    public function status(User $user): array
    {
        $today = today();

        $todaySignIn = DailySignIn::query()
            ->where('user_id', $user->id)
            ->whereDate('sign_in_date', $today)
            ->first();

        $latestSignIn = DailySignIn::query()
            ->where('user_id', $user->id)
            ->latest('sign_in_date')
            ->first();

        $signedToday = $todaySignIn !== null;
        $claimableStreakDays = $signedToday
            ? (int) $todaySignIn->streak_days
            : $this->calculateNextStreakDays($latestSignIn, $today);

        return [
            'signed_today' => $signedToday,
            'can_claim' => ! $signedToday,
            'current_streak_days' => (int) ($latestSignIn?->streak_days ?? 0),
            'claimable_streak_days' => $claimableStreakDays,
            'today_sign_in' => $todaySignIn,
            'latest_sign_in' => $latestSignIn,
            'matched_rule' => $this->matchRewardRule($claimableStreakDays),
            'current_identity' => $user->currentSignInIdentity(),
        ];
    }

    /**
     * 查询指定月份的签到日历状态。
     *
     * @return array<string, mixed>
     */
    public function calendar(User $user, ?string $month = null): array
    {
        $monthStart = $month
            ? Carbon::createFromFormat('Y-m', $month)->startOfMonth()
            : today()->startOfMonth();
        $monthEnd = $monthStart->copy()->endOfMonth();
        $today = today();
        // 补签卡只允许修补当前自然月，查看历史月份时不再开放补签入口。
        $isCurrentMonth = $monthStart->isSameMonth($today);

        $signIns = DailySignIn::query()
            ->where('user_id', $user->id)
            ->whereBetween('sign_in_date', [$monthStart->toDateString(), $monthEnd->toDateString()])
            ->get()
            ->keyBy(fn (DailySignIn $signIn): string => $signIn->sign_in_date->toDateString());

        $days = [];
        for ($date = $monthStart->copy(); $date->lte($monthEnd); $date->addDay()) {
            $dateKey = $date->toDateString();
            $signIn = $signIns->get($dateKey);
            $isFuture = $date->gt($today);

            $days[] = [
                'date' => $dateKey,
                'day' => (int) $date->day,
                'weekday' => (int) $date->dayOfWeek,
                'signed' => $signIn !== null,
                'is_today' => $date->isSameDay($today),
                'is_future' => $isFuture,
                'can_makeup' => $isCurrentMonth && $signIn === null && $date->lt($today),
                'is_makeup' => (bool) ($signIn?->is_makeup ?? false),
                'streak_days' => (int) ($signIn?->streak_days ?? 0),
                'reward_text' => $signIn ? $this->buildRewardText($signIn) : '',
            ];
        }

        return [
            'month' => $monthStart->format('Y-m'),
            'month_label' => $monthStart->format('Y年n月'),
            'prev_month' => $monthStart->copy()->subMonth()->format('Y-m'),
            'next_month' => $monthStart->copy()->addMonth()->format('Y-m'),
            'days' => $days,
            'reward_rules' => $this->rewardRulePreview(),
            'makeup_card_count' => $this->availableSignRepairCardCount($user),
            'sign_repair_card_item' => $this->activeSignRepairCard()?->only(['id', 'name', 'slug', 'icon', 'price', 'description']),
        ];
    }

    /**
     * 领取每日签到奖励。
     */
    public function claim(User $user, ?int $roomId = null): DailySignIn
    {
        return DB::transaction(function () use ($user, $roomId): DailySignIn {
            $today = today();

            // 锁定用户行，将同一用户的并发签到串行化，避免“查无今日记录”时并发重复发奖。
            $lockedUser = User::query()
                ->whereKey($user->id)
                ->lockForUpdate()
                ->firstOrFail();

            $existingSignIn = DailySignIn::query()
                ->where('user_id', $lockedUser->id)
                ->whereDate('sign_in_date', $today)
                ->lockForUpdate()
                ->first();

            if ($existingSignIn !== null) {
                return $existingSignIn->load('rewardRule');
            }

            $latestSignIn = DailySignIn::query()
                ->where('user_id', $lockedUser->id)
                ->whereDate('sign_in_date', '<', $today)
                ->latest('sign_in_date')
                ->lockForUpdate()
                ->first();

            $streakDays = $this->calculateNextStreakDays($latestSignIn, $today);
            $rewardRule = $this->matchRewardRule($streakDays);

            $dailySignIn = DailySignIn::query()->create([
                'user_id' => $lockedUser->id,
                'room_id' => $roomId,
                'sign_in_date' => $today->toDateString(),
                'streak_days' => $streakDays,
                'reward_rule_id' => $rewardRule?->id,
                'gold_reward' => (int) ($rewardRule?->gold_reward ?? 0),
                'exp_reward' => (int) ($rewardRule?->exp_reward ?? 0),
                'charm_reward' => (int) ($rewardRule?->charm_reward ?? 0),
                'identity_badge_code' => $rewardRule?->identity_badge_code,
                'identity_badge_name' => $rewardRule?->identity_badge_name,
                'identity_badge_icon' => $rewardRule?->identity_badge_icon,
                'identity_badge_color' => $rewardRule?->identity_badge_color,
            ]);

            $this->grantRewards($lockedUser, $dailySignIn, $roomId);
            $this->refreshSignInIdentityBadge($lockedUser, $rewardRule, $streakDays);

            return $dailySignIn->load('rewardRule');
        });
    }

    /**
     * 使用补签卡补签指定历史日期。
     */
    public function makeup(User $user, string $targetDate, ?int $roomId = null): DailySignIn
    {
        return DB::transaction(function () use ($user, $targetDate, $roomId): DailySignIn {
            $date = Carbon::parse($targetDate)->startOfDay();

            if ($date->greaterThanOrEqualTo(today())) {
                throw ValidationException::withMessages(['target_date' => '只能补签今天之前的漏签日期。']);
            }

            // 后端再次限制补签月份，避免绕过前端日历直接提交历史月份。
            if (! $date->isSameMonth(today())) {
                throw ValidationException::withMessages(['target_date' => '补签卡只能补签本月的未签到日期。']);
            }

            // 锁定用户行，保证补签卡消耗和签到记录创建不会并发重复执行。
            $lockedUser = User::query()
                ->whereKey($user->id)
                ->lockForUpdate()
                ->firstOrFail();

            $existingSignIn = DailySignIn::query()
                ->where('user_id', $lockedUser->id)
                ->whereDate('sign_in_date', $date)
                ->lockForUpdate()
                ->first();

            if ($existingSignIn !== null) {
                throw ValidationException::withMessages(['target_date' => '这一天已经签到过，不能重复补签。']);
            }

            $repairCard = $this->nextAvailableSignRepairCard($lockedUser);
            if ($repairCard === null) {
                throw ValidationException::withMessages(['target_date' => '没有可用补签卡，请先购买补签卡。']);
            }

            $latestBeforeTarget = DailySignIn::query()
                ->where('user_id', $lockedUser->id)
                ->whereDate('sign_in_date', '<', $date)
                ->latest('sign_in_date')
                ->lockForUpdate()
                ->first();
            $streakDays = $this->calculateNextStreakDays($latestBeforeTarget, $date);

            $dailySignIn = DailySignIn::query()->create([
                'user_id' => $lockedUser->id,
                'room_id' => $roomId,
                'is_makeup' => true,
                'makeup_purchase_id' => $repairCard->id,
                'makeup_at' => now(),
                'sign_in_date' => $date->toDateString(),
                'streak_days' => $streakDays,
                'reward_rule_id' => null,
                'gold_reward' => 0,
                'exp_reward' => 0,
                'charm_reward' => 0,
                'identity_badge_code' => null,
                'identity_badge_name' => null,
                'identity_badge_icon' => null,
                'identity_badge_color' => null,
            ]);

            // 补签卡与补签记录必须在同一事务里完成状态流转。
            $repairCard->update(['status' => 'used', 'used_at' => now()]);
            $this->recalculateFutureStreaks($lockedUser, $date);
            $currentStreakDays = $this->latestStreakDays($lockedUser);
            $rewardRule = $this->matchRewardRule($currentStreakDays);
            $dailySignIn->update([
                'reward_rule_id' => $rewardRule?->id,
                'gold_reward' => (int) ($rewardRule?->gold_reward ?? 0),
                'exp_reward' => (int) ($rewardRule?->exp_reward ?? 0),
                'charm_reward' => (int) ($rewardRule?->charm_reward ?? 0),
                'identity_badge_code' => $rewardRule?->identity_badge_code,
                'identity_badge_name' => $rewardRule?->identity_badge_name,
                'identity_badge_icon' => $rewardRule?->identity_badge_icon,
                'identity_badge_color' => $rewardRule?->identity_badge_color,
            ]);
            $this->grantRewards(
                $lockedUser,
                $dailySignIn->fresh(),
                $roomId,
                "补签 {$date->toDateString()}：当前连续签到 {$currentStreakDays} 天"
            );
            $this->refreshIdentityFromLatestSignIn($lockedUser);

            return $dailySignIn->fresh('rewardRule');
        });
    }

    /**
     * 计算下一次签到应获得的连续天数。
     */
    private function calculateNextStreakDays(?DailySignIn $latestSignIn, ?Carbon $targetDate = null): int
    {
        $targetDate ??= today();

        if ($latestSignIn === null) {
            return 1;
        }

        $yesterday = $targetDate->copy()->subDay()->toDateString();

        if ($latestSignIn->sign_in_date?->toDateString() === $yesterday) {
            // 签到满一年后开启新周期，下一天重新从第 1 天计算。
            if ((int) $latestSignIn->streak_days >= $this->daysInSignInYear($latestSignIn)) {
                return 1;
            }

            return (int) $latestSignIn->streak_days + 1;
        }

        return 1;
    }

    /**
     * 按签到记录所在年份计算当年天数，闰年为 366 天。
     */
    private function daysInSignInYear(DailySignIn $dailySignIn): int
    {
        return $dailySignIn->sign_in_date?->isLeapYear() ? 366 : 365;
    }

    /**
     * 匹配当前连续天数可用的最高奖励规则。
     */
    private function matchRewardRule(int $streakDays): ?SignInRewardRule
    {
        return SignInRewardRule::query()
            ->where('is_enabled', true)
            ->where('streak_days', '<=', $streakDays)
            ->orderByDesc('streak_days')
            ->first();
    }

    /**
     * 按签到记录快照发放金币、经验和魅力。
     */
    private function grantRewards(User $user, DailySignIn $dailySignIn, ?int $roomId, ?string $remark = null): void
    {
        $remark ??= "每日签到：连续 {$dailySignIn->streak_days} 天";

        $this->currencyService->change($user, 'gold', $dailySignIn->gold_reward, CurrencySource::SIGN_IN, $remark, $roomId);
        $this->currencyService->change($user, 'exp', $dailySignIn->exp_reward, CurrencySource::SIGN_IN, $remark, $roomId);
        $this->currencyService->change($user, 'charm', $dailySignIn->charm_reward, CurrencySource::SIGN_IN, $remark, $roomId);
    }

    /**
     * 刷新用户当前签到身份徽章。
     */
    private function refreshSignInIdentityBadge(User $user, ?SignInRewardRule $rewardRule, int $streakDays): void
    {
        UserIdentityBadge::query()
            ->where('user_id', $user->id)
            ->where('source', UserIdentityBadge::SOURCE_SIGN_IN)
            ->where('is_active', true)
            ->update(['is_active' => false]);

        if ($rewardRule?->identity_badge_code === null || $rewardRule->identity_badge_name === null) {
            return;
        }

        // 同一来源同一徽章唯一，重复命中时更新状态和连续天数快照即可。
        UserIdentityBadge::query()->updateOrCreate(
            [
                'user_id' => $user->id,
                'source' => UserIdentityBadge::SOURCE_SIGN_IN,
                'badge_code' => $rewardRule->identity_badge_code,
            ],
            [
                'badge_name' => $rewardRule->identity_badge_name,
                'badge_icon' => $rewardRule->identity_badge_icon,
                'badge_color' => $rewardRule->identity_badge_color,
                'acquired_at' => now(),
                // 身份有效期由后台规则控制，0 表示长期有效。
                'expires_at' => $rewardRule->identity_duration_days > 0
                    ? now()->addDays($rewardRule->identity_duration_days)
                    : null,
                'is_active' => true,
                'metadata' => [
                    'streak_days' => $streakDays,
                    'reward_rule_id' => $rewardRule->id,
                ],
            ]
        );
    }

    /**
     * 按日期向后重算连续天数，补齐断点后让后续日历展示同步变化。
     */
    private function recalculateFutureStreaks(User $user, Carbon $fromDate): void
    {
        $records = DailySignIn::query()
            ->where('user_id', $user->id)
            ->whereDate('sign_in_date', '>=', $fromDate)
            ->orderBy('sign_in_date')
            ->lockForUpdate()
            ->get();

        $previous = DailySignIn::query()
            ->where('user_id', $user->id)
            ->whereDate('sign_in_date', '<', $fromDate)
            ->latest('sign_in_date')
            ->lockForUpdate()
            ->first();

        $previousDate = $previous?->sign_in_date;
        $previousStreak = (int) ($previous?->streak_days ?? 0);

        $records->each(function (DailySignIn $record) use (&$previousDate, &$previousStreak): void {
            $streakDays = $previousDate?->copy()->addDay()->toDateString() === $record->sign_in_date?->toDateString()
                ? $previousStreak + 1
                : 1;

            if ((int) $record->streak_days !== $streakDays) {
                // 只重算连续天数快照，不追溯变更已发放奖励流水。
                $record->update(['streak_days' => $streakDays]);
            }

            $previousDate = $record->sign_in_date;
            $previousStreak = $streakDays;
        });
    }

    /**
     * 使用最新签到记录刷新当前签到身份。
     */
    private function refreshIdentityFromLatestSignIn(User $user): void
    {
        $latestSignIn = DailySignIn::query()
            ->where('user_id', $user->id)
            ->latest('sign_in_date')
            ->first();

        if ($latestSignIn === null) {
            return;
        }

        $this->refreshSignInIdentityBadge($user, $this->matchRewardRule((int) $latestSignIn->streak_days), (int) $latestSignIn->streak_days);
    }

    /**
     * 查询用户最新签到记录的连续天数。
     */
    private function latestStreakDays(User $user): int
    {
        return (int) DailySignIn::query()
            ->where('user_id', $user->id)
            ->latest('sign_in_date')
            ->value('streak_days');
    }

    /**
     * 查询用户下一张可消耗的补签卡。
     */
    private function nextAvailableSignRepairCard(User $user): ?UserPurchase
    {
        return UserPurchase::query()
            ->where('user_id', $user->id)
            ->where('status', 'active')
            ->whereHas('shopItem', fn ($query) => $query->where('type', ShopItem::TYPE_SIGN_REPAIR))
            ->oldest()
            ->lockForUpdate()
            ->first();
    }

    /**
     * 查询用户可用补签卡数量。
     */
    private function availableSignRepairCardCount(User $user): int
    {
        return UserPurchase::query()
            ->where('user_id', $user->id)
            ->where('status', 'active')
            ->whereHas('shopItem', fn ($query) => $query->where('type', ShopItem::TYPE_SIGN_REPAIR))
            ->count();
    }

    /**
     * 查询当前上架的补签卡商品。
     */
    private function activeSignRepairCard(): ?ShopItem
    {
        return ShopItem::query()
            ->where('type', ShopItem::TYPE_SIGN_REPAIR)
            ->where('is_active', true)
            ->orderBy('sort_order')
            ->first();
    }

    /**
     * 查询启用中的签到奖励档位，供前台展示目标奖励。
     *
     * @return array<int, array<string, mixed>>
     */
    private function rewardRulePreview(): array
    {
        return SignInRewardRule::query()
            ->where('is_enabled', true)
            ->orderBy('streak_days')
            ->get()
            ->map(fn (SignInRewardRule $rule): array => [
                'streak_days' => (int) $rule->streak_days,
                'gold_reward' => (int) $rule->gold_reward,
                'exp_reward' => (int) $rule->exp_reward,
                'charm_reward' => (int) $rule->charm_reward,
                'identity_badge_name' => $rule->identity_badge_name,
                'identity_badge_icon' => $rule->identity_badge_icon,
                'identity_badge_color' => $rule->identity_badge_color,
                'identity_duration_days' => (int) $rule->identity_duration_days,
            ])
            ->all();
    }

    /**
     * 生成日历内展示的签到奖励文本。
     */
    private function buildRewardText(DailySignIn $dailySignIn): string
    {
        $items = [];
        if ($dailySignIn->gold_reward > 0) {
            $items[] = $dailySignIn->gold_reward.'金';
        }
        if ($dailySignIn->exp_reward > 0) {
            $items[] = $dailySignIn->exp_reward.'经验';
        }
        if ($dailySignIn->charm_reward > 0) {
            $items[] = $dailySignIn->charm_reward.'魅力';
        }

        return $items === [] ? '已记录' : implode(' + ', $items);
    }
}
