<?php

/**
 * 文件功能：用户个人装扮统一管理服务
 * 负责装扮的购买、激活、过期清理、以及向前端广播载荷注入装饰信息。
 * 所有装扮变更经由此服务，确保 users.active_decorations JSON 字段一致性。
 *
 * 当前支持的装扮槽位（存储在 active_decorations 的 key）：
 *   - bubble      : 消息气泡边框样式
 *   - name_color  : 昵称颜色效果
 *   - avatar_frame: 头像装饰边框
 *   - text_color  : 消息文字颜色特效
 *
 * @author   ChatRoom Laravel
 *
 * @version  1.0.0
 */

namespace App\Services;

use App\Enums\CurrencySource;
use App\Models\ShopItem;
use App\Models\User;
use App\Models\UserPurchase;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;

/**
 * 类功能：统一管理用户个人装扮的购买、激活、过期清理及广播数据注入。
 */
class DecorationService
{
    /**
     * 商店商品 type → 装扮槽位映射。
     * 以后新增装扮类型，在此加一行即可。
     */
    private const TYPE_TO_SLOT = [
        'msg_bubble' => 'bubble',
        'msg_name_color' => 'name_color',
        'avatar_frame' => 'avatar_frame',
        'msg_text_color' => 'text_color',
    ];

    /**
     * @param  UserCurrencyService  $currencyService  统一积分变更服务
     */
    public function __construct(
        private readonly UserCurrencyService $currencyService,
    ) {}

    /**
     * 购买装扮：扣金币、写购买记录、更新 users.active_decorations。
     *
     * 同槽位的旧装扮会被新购买覆盖（旧装扮不退款），不同槽位可并行持有。
     * 若购买的是已激活的同款样式，则自动叠加天数而非覆盖重置。
     *
     * @param  User  $user  购买用户
     * @param  ShopItem  $item  装扮商品
     * @param  int  $quantity  购买份数
     * @return array{ok:bool, message:string, balance_after?:int, slot?:string, style?:string, expires_at?:string}
     */
    public function purchase(User $user, ShopItem $item, int $quantity = 1): array
    {
        // 根据商品类型映射到对应槽位
        $slot = self::TYPE_TO_SLOT[$item->type] ?? null;
        if (! $slot) {
            return ['ok' => false, 'message' => '未知装扮类型'];
        }

        $totalPrice = $item->price * $quantity;

        // 校验金币余额
        if ($user->jjb < $totalPrice) {
            return ['ok' => false, 'message' => "金币不足，购买 {$quantity} 份 [{$item->name}] 需要 {$totalPrice} 金币，当前仅有 {$user->jjb} 金币。"];
        }

        // 计算过期时间（至少 1 天）
        $days = max(1, (int) ($item->duration_days ?? 1));
        $totalDays = $days * $quantity;

        // 检查同一槽位是否已激活相同样式 → 叠加天数
        $decorations = $this->getActiveDecorations($user);
        $isSameActive = ! empty($decorations[$slot])
            && ($decorations[$slot]['style'] ?? '') === $item->slug;

        if ($isSameActive) {
            // 在现有到期时间上追加天数
            $existingExpires = Carbon::parse($decorations[$slot]['expires_at']);
            $expiresAt = $existingExpires->copy()->addDays($totalDays);
        } else {
            $expiresAt = Carbon::now()->addDays($totalDays);
        }

        // 按装扮类型使用不同的流水来源标识，便于后台按类型筛选消费记录
        $source = match ($item->type) {
            'msg_bubble' => CurrencySource::MSG_BUBBLE_BUY,
            'msg_name_color' => CurrencySource::MSG_NAME_COLOR_BUY,
            'msg_text_color' => CurrencySource::MSG_TEXT_COLOR_BUY,
            'avatar_frame' => CurrencySource::AVATAR_FRAME_BUY,
            default => CurrencySource::MSG_DECORATION_BUY,
        };

        // 事务包裹：扣金币、写购买记录、更新激活状态三步原子操作
        DB::transaction(function () use ($user, $item, $slot, $totalPrice, $totalDays, $expiresAt, $source) {
            // ① 通过统一积分服务扣除金币（含流水记录）
            $this->currencyService->change(
                $user, 'gold', -$totalPrice, $source,
                "购买装扮：{$item->name}（{$totalDays}天）"
            );

            // ② 写入购买记录（用于后台统计与用户回溯）
            UserPurchase::create([
                'user_id' => $user->id,
                'shop_item_id' => $item->id,
                'status' => 'active',
                'price_paid' => $totalPrice,
                'expires_at' => $expiresAt,
            ]);

            // ③ 更新用户 active_decorations JSON 字段（同槽位合并，不同槽位追加）
            $decorations = $this->getActiveDecorations($user);
            $decorations[$slot] = [
                'style' => $item->slug,
                'expires_at' => $expiresAt->toIso8601String(),
            ];
            $user->active_decorations = $decorations;
            $user->save();
        });

        // 重新读取最新余额，避免缓存脏数据
        $balanceAfter = (int) $user->fresh()->jjb;

        // 计算叠加后的总天数显示（如果是续费，显示累计总天数）
        $displayDays = $isSameActive
            ? (int) Carbon::now()->diffInDays(Carbon::parse($expiresAt), false) + 1
            : $totalDays;

        return [
            'ok' => true,
            'message' => "购买成功！{$item->icon} {$item->name} 已激活（{$displayDays}天有效）",
            'balance_after' => $balanceAfter,
            'slot' => $slot,
            'style' => $item->slug,
            'expires_at' => $expiresAt->toIso8601String(),
            'quantity' => $quantity,
        ];
    }

    /**
     * 获取用户当前所有激活的装扮，同时自动清理已过期项。
     *
     * 采用懒过期策略，在每次读取时检查 expires_at，不依赖定时任务。
     * 如果清理了过期数据会自动写回 users.active_decorations。
     *
     * @param  User  $user  目标用户
     * @return array<string, array{style:string, expires_at:string}> 返回干净的装扮列表
     */
    public function getActiveDecorations(User $user): array
    {
        $decorations = $user->active_decorations ?? [];

        // 非数组（null 或格式异常）直接返回空
        if (! is_array($decorations)) {
            return [];
        }

        $now = Carbon::now();
        $changed = false;

        foreach ($decorations as $slot => $data) {
            // 格式校验：必须包含 expires_at 字段
            if (! is_array($data) || empty($data['expires_at'])) {
                unset($decorations[$slot]);
                $changed = true;

                continue;
            }

            // 解析过期时间并与当前时间比较
            try {
                $expiresAt = Carbon::parse($data['expires_at']);
                if ($expiresAt->isPast()) {
                    unset($decorations[$slot]);
                    $changed = true;
                }
            } catch (\Exception $e) {
                // 时间格式异常也视为无效，清理之
                unset($decorations[$slot]);
                $changed = true;
            }
        }

        // 有过期项被清理时写回数据库
        if ($changed) {
            $user->active_decorations = $decorations;
            $user->save();
        }

        return $decorations;
    }

    /**
     * 获取消息广播 payload 需要携带的装扮字段。
     *
     * 消息广播需要气泡样式（msg_bubble）、昵称颜色（msg_name_color）、文字颜色特效（msg_text_color）
     * 以及头像框（avatar_frame），前端据此渲染发送者头像的装饰边框。
     *
     * @param  User  $user  消息发送者
     * @return array{msg_bubble?:string, msg_name_color?:string, msg_text_color?:string, avatar_frame?:string}
     */
    public function getDecorationsForMessage(User $user): array
    {
        $decorations = $this->getActiveDecorations($user);
        $result = [];

        if (! empty($decorations['bubble']['style'])) {
            $result['msg_bubble'] = $decorations['bubble']['style'];
        }
        if (! empty($decorations['name_color']['style'])) {
            $result['msg_name_color'] = $decorations['name_color']['style'];
        }
        if (! empty($decorations['text_color']['style'])) {
            $result['msg_text_color'] = $decorations['text_color']['style'];
        }
        if (! empty($decorations['avatar_frame']['style'])) {
            $result['avatar_frame'] = $decorations['avatar_frame']['style'];
        }

        return $result;
    }

    /**
     * 获取在线用户 Presence 载荷需要携带的装扮字段。
     *
     * 在线列表中主要展示头像框（avatar_frame）和昵称颜色（name_color），
     * 气泡样式仅作用于消息，不需要在用户列表中展示。
     *
     * @param  User  $user  目标用户
     * @return array{avatar_frame?:string, name_color?:string}
     */
    public function getDecorationsForPresence(User $user): array
    {
        $decorations = $this->getActiveDecorations($user);
        $result = [];

        if (! empty($decorations['avatar_frame']['style'])) {
            $result['avatar_frame'] = $decorations['avatar_frame']['style'];
        }
        if (! empty($decorations['name_color']['style'])) {
            $result['name_color'] = $decorations['name_color']['style'];
        }

        return $result;
    }
}
