<?php

/**
 * 文件功能：管理员聊天室实时命令控制器
 *
 * 提供管理员在聊天室内对用户执行的管理操作：
 * 警告(=J)、踢出(=T)、禁言(=B)、冻结(=Y)、查看私信(=S)、职务公屏讲话。
 *
 * 对应原 ASP 文件：DOUSER.ASP / KILLUSER.ASP / LOCKIP.ASP / NEWSAY.ASP
 *
 * @author   ChatRoom Laravel
 *
 * @version  1.0.0
 */

namespace App\Http\Controllers;

use App\Enums\CurrencySource;
use App\Events\BrowserRefreshRequested;
use App\Events\EffectBroadcast;
use App\Events\MessageSent;
use App\Jobs\SaveMessageJob;
use App\Models\Message;
use App\Models\PositionAuthorityLog;
use App\Models\Room;
use App\Models\Sysparam;
use App\Models\User;
use App\Services\ChatStateService;
use App\Services\PositionPermissionService;
use App\Services\UserCurrencyService;
use App\Support\ChatContentSanitizer;
use App\Support\PositionPermissionRegistry;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Redis;
use Illuminate\Validation\Rule;

/**
 * 类功能：处理聊天室内的实时管理命令与部分职务奖励操作。
 */
class AdminCommandController extends Controller
{
    /**
     * 构造函数：注入聊天状态服务
     */
    public function __construct(
        private readonly ChatStateService $chatState,
        private readonly UserCurrencyService $currencyService,
        private readonly PositionPermissionService $positionPermissionService,
    ) {}

    /**
     * 警告用户（=J 理由）
     *
     * 向目标用户发送系统警告消息，所有人可见。
     *
     * @param  Request  $request  请求对象，需包含 username, room_id, reason
     * @return JsonResponse 操作结果
     */
    public function warn(Request $request): JsonResponse
    {
        $request->validate([
            'username' => 'required|string',
            'room_id' => 'required|integer',
            'reason' => 'nullable|string|max:200',
        ]);

        $admin = Auth::user();
        $targetUsername = $request->input('username');
        $roomId = (int) $request->input('room_id');
        $roomAuthorization = $this->authorizeManagedRoom($roomId, $admin);
        if (! $roomAuthorization['ok']) {
            return response()->json(['status' => 'error', 'message' => $roomAuthorization['message']], 403);
        }

        $reason = ChatContentSanitizer::htmlText($request->input('reason', '请注意言行'));
        $safeTargetUsername = ChatContentSanitizer::htmlText($targetUsername);
        $operatorDisplay = $this->buildOperatorDisplayHtml($admin);

        // 权限检查：必须拥有职务权限，且不能处理职务高于自己的用户。
        $authorization = $this->authorizeModerationAction($admin, $targetUsername, PositionPermissionRegistry::USER_WARN, '警告');
        if (! $authorization['ok']) {
            return response()->json(['status' => 'error', 'message' => $authorization['message']], 403);
        }

        $targetAuthorization = $this->authorizeTargetOnlineInRoom($roomId, $targetUsername);
        if (! $targetAuthorization['ok']) {
            return response()->json(['status' => 'error', 'message' => $targetAuthorization['message']], 403);
        }

        // 广播警告消息
        $msg = [
            'id' => $this->chatState->nextMessageId($roomId),
            'room_id' => $roomId,
            'from_user' => '系统传音',
            'to_user' => '大家',
            'content' => "⚠️ {$operatorDisplay} 警告 <b>{$safeTargetUsername}</b>：{$reason}",
            'is_secret' => false,
            'font_color' => '#dc2626',
            'action' => '',
            'sent_at' => now()->toDateTimeString(),
        ];
        $this->chatState->pushMessage($roomId, $msg);
        broadcast(new MessageSent($roomId, $msg));
        SaveMessageJob::dispatch($msg);

        // 给被警告用户补一条私聊提示，并复用右下角 toast 通知。
        $this->pushTargetToastMessage(
            roomId: (int) $roomId,
            targetUsername: $targetUsername,
            content: "⚠️ {$operatorDisplay} 警告了你：{$reason}",
            title: '⚠️ 收到警告',
            toastMessage: "{$operatorDisplay} 警告了你：{$reason}",
            color: '#f59e0b',
            icon: '⚠️',
        );

        return response()->json(['status' => 'success', 'message' => "已警告 {$targetUsername}"]);
    }

    /**
     * 踢出用户（=T 理由）
     *
     * 将目标用户从聊天室踢出，清除其 Redis 在线状态。
     *
     * @param  Request  $request  请求对象，需包含 username, room_id, reason
     * @return JsonResponse 操作结果
     */
    public function kick(Request $request): JsonResponse
    {
        $request->validate([
            'username' => 'required|string',
            'room_id' => 'required|integer',
            'reason' => 'nullable|string|max:200',
        ]);

        $admin = Auth::user();
        $targetUsername = $request->input('username');
        $roomId = (int) $request->input('room_id');
        $roomAuthorization = $this->authorizeManagedRoom($roomId, $admin);
        if (! $roomAuthorization['ok']) {
            return response()->json(['status' => 'error', 'message' => $roomAuthorization['message']], 403);
        }

        $reason = ChatContentSanitizer::htmlText($request->input('reason', '违反聊天室规则'));
        $safeTargetUsername = ChatContentSanitizer::htmlText($targetUsername);
        $operatorDisplay = $this->buildOperatorDisplayHtml($admin);

        // 权限检查：必须拥有职务权限，且不能处理职务高于自己的用户。
        $authorization = $this->authorizeModerationAction($admin, $targetUsername, PositionPermissionRegistry::USER_KICK, '踢出');
        if (! $authorization['ok']) {
            return response()->json(['status' => 'error', 'message' => $authorization['message']], 403);
        }

        $targetAuthorization = $this->authorizeTargetOnlineInRoom($roomId, $targetUsername);
        if (! $targetAuthorization['ok']) {
            return response()->json(['status' => 'error', 'message' => $targetAuthorization['message']], 403);
        }

        // 在强制踢出前补发目标私聊提示，尽量让对方先看到 toast。
        $this->pushTargetToastMessage(
            roomId: (int) $roomId,
            targetUsername: $targetUsername,
            content: "🚫 {$operatorDisplay} 已将你踢出聊天室。原因：{$reason}",
            title: '🚫 已被踢出',
            toastMessage: "{$operatorDisplay} 已将你踢出聊天室。<br>原因：{$reason}",
            color: '#ef4444',
            icon: '🚫',
        );

        // 从 Redis 在线列表移除
        $this->chatState->userLeave($roomId, $targetUsername);

        // 广播踢出消息（通知前端强制该用户跳转）
        $msg = [
            'id' => $this->chatState->nextMessageId($roomId),
            'room_id' => $roomId,
            'from_user' => '系统传音',
            'to_user' => '大家',
            'content' => "🚫 {$operatorDisplay} 已将 <b>{$safeTargetUsername}</b> 踢出聊天室。原因：{$reason}",
            'is_secret' => false,
            'font_color' => '#dc2626',
            'action' => '',
            'sent_at' => now()->toDateTimeString(),
        ];
        $this->chatState->pushMessage($roomId, $msg);
        broadcast(new MessageSent($roomId, $msg));
        SaveMessageJob::dispatch($msg);

        // 广播踢出事件（前端监听后强制跳转）
        broadcast(new \App\Events\UserKicked($roomId, $targetUsername, $reason));

        return response()->json(['status' => 'success', 'message' => "已踢出 {$targetUsername}"]);
    }

    /**
     * 禁言用户（=B 分钟数）
     *
     * 使用 Redis TTL 自动过期机制，禁止用户发言指定分钟数。
     *
     * @param  Request  $request  请求对象，需包含 username, room_id, duration
     * @return JsonResponse 操作结果
     */
    public function mute(Request $request): JsonResponse
    {
        $request->validate([
            'username' => 'required|string',
            'room_id' => 'required|integer',
            'duration' => 'required|integer|min:1|max:1440',
        ]);

        $admin = Auth::user();
        $targetUsername = $request->input('username');
        $roomId = (int) $request->input('room_id');
        $roomAuthorization = $this->authorizeManagedRoom($roomId, $admin);
        if (! $roomAuthorization['ok']) {
            return response()->json(['status' => 'error', 'message' => $roomAuthorization['message']], 403);
        }

        $duration = $request->input('duration');
        $safeTargetUsername = ChatContentSanitizer::htmlText($targetUsername);
        $operatorDisplay = $this->buildOperatorDisplayHtml($admin);
        $operatorLabel = $this->buildOperatorIdentityLabel($admin).' '.$admin->username;

        // 权限检查：必须拥有职务权限，且不能处理职务高于自己的用户。
        $authorization = $this->authorizeModerationAction($admin, $targetUsername, PositionPermissionRegistry::USER_MUTE, '禁言');
        if (! $authorization['ok']) {
            return response()->json(['status' => 'error', 'message' => $authorization['message']], 403);
        }

        $targetAuthorization = $this->authorizeTargetOnlineInRoom($roomId, $targetUsername);
        if (! $targetAuthorization['ok']) {
            return response()->json(['status' => 'error', 'message' => $targetAuthorization['message']], 403);
        }

        // 设置 Redis 禁言标记，TTL 自动过期
        $muteKey = "mute:{$roomId}:{$targetUsername}";
        Redis::setex($muteKey, $duration * 60, now()->toDateTimeString());

        // 广播禁言消息
        $msg = [
            'id' => $this->chatState->nextMessageId($roomId),
            'room_id' => $roomId,
            'from_user' => '系统传音',
            'to_user' => '大家',
            'content' => "🔇 {$operatorDisplay} 已将 <b>{$safeTargetUsername}</b> 禁言 {$duration} 分钟。",
            'is_secret' => false,
            'font_color' => '#dc2626',
            'action' => '',
            'sent_at' => now()->toDateTimeString(),
        ];
        $this->chatState->pushMessage($roomId, $msg);
        broadcast(new MessageSent($roomId, $msg));
        SaveMessageJob::dispatch($msg);

        // 给被禁言用户补一条私聊提示，并复用右下角 toast 通知。
        $this->pushTargetToastMessage(
            roomId: (int) $roomId,
            targetUsername: $targetUsername,
            content: "🔇 {$operatorDisplay} 已将你禁言 {$duration} 分钟。",
            title: '🔇 已被禁言',
            toastMessage: "{$operatorDisplay} 已将你禁言 <b>{$duration}</b> 分钟。",
            color: '#6366f1',
            icon: '🔇',
        );

        // 广播禁言事件（前端禁用输入框）
        broadcast(new \App\Events\UserMuted(
            roomId: $roomId,
            username: $targetUsername,
            muteTime: $duration,
            message: "{$operatorLabel} 已将 [{$targetUsername}] 禁言 {$duration} 分钟。",
            operator: $operatorLabel,
        ));

        return response()->json(['status' => 'success', 'message' => "已禁言 {$targetUsername} {$duration} 分钟"]);
    }

    /**
     * 冻结用户账号（=Y 理由）
     *
     * 将用户账号状态设为冻结，禁止登录。
     *
     * @param  Request  $request  请求对象，需包含 username, reason
     * @return JsonResponse 操作结果
     */
    public function freeze(Request $request): JsonResponse
    {
        $request->validate([
            'username' => 'required|string',
            'room_id' => 'required|integer',
            'reason' => 'nullable|string|max:200',
        ]);

        $admin = Auth::user();
        $targetUsername = $request->input('username');
        $roomId = (int) $request->input('room_id');
        $roomAuthorization = $this->authorizeManagedRoom($roomId, $admin);
        if (! $roomAuthorization['ok']) {
            return response()->json(['status' => 'error', 'message' => $roomAuthorization['message']], 403);
        }

        $reason = ChatContentSanitizer::htmlText($request->input('reason', '违反聊天室规则'));
        $safeTargetUsername = ChatContentSanitizer::htmlText($targetUsername);
        $operatorDisplay = $this->buildOperatorDisplayHtml($admin);

        // 权限检查：必须拥有职务权限，且不能处理职务高于自己的用户。
        $authorization = $this->authorizeModerationAction($admin, $targetUsername, PositionPermissionRegistry::USER_FREEZE, '冻结');
        if (! $authorization['ok']) {
            return response()->json(['status' => 'error', 'message' => $authorization['message']], 403);
        }

        $targetAuthorization = $this->authorizeTargetOnlineInRoom($roomId, $targetUsername);
        if (! $targetAuthorization['ok']) {
            return response()->json(['status' => 'error', 'message' => $targetAuthorization['message']], 403);
        }

        // 冻结用户账号（将等级设为 -1 表示冻结）
        $target = $authorization['target'];
        $target->user_level = -1;
        $target->save();

        // 先给被冻结用户补发私聊提示，再将其移出各房间并强制下线。
        $this->pushTargetToastMessage(
            roomId: (int) $roomId,
            targetUsername: $targetUsername,
            content: "🧊 {$operatorDisplay} 已冻结你的账号。原因：{$reason}",
            title: '🧊 账号已冻结',
            toastMessage: "{$operatorDisplay} 已冻结你的账号。<br>原因：{$reason}",
            color: '#3b82f6',
            icon: '🧊',
        );

        // 从所有房间移除
        $rooms = $this->chatState->getUserRooms($targetUsername);
        foreach ($rooms as $rid) {
            $this->chatState->userLeave($rid, $targetUsername);
        }

        // 广播冻结消息
        $msg = [
            'id' => $this->chatState->nextMessageId($roomId),
            'room_id' => $roomId,
            'from_user' => '系统传音',
            'to_user' => '大家',
            'content' => "🧊 {$operatorDisplay} 已冻结 <b>{$safeTargetUsername}</b> 的账号。原因：{$reason}",
            'is_secret' => false,
            'font_color' => '#dc2626',
            'action' => '',
            'sent_at' => now()->toDateTimeString(),
        ];
        $this->chatState->pushMessage($roomId, $msg);
        broadcast(new MessageSent($roomId, $msg));
        SaveMessageJob::dispatch($msg);

        // 广播踢出事件
        broadcast(new \App\Events\UserKicked($roomId, $targetUsername, "账号已被冻结：{$reason}"));

        return response()->json(['status' => 'success', 'message' => "已冻结 {$targetUsername} 的账号"]);
    }

    /**
     * 查看用户私信（=S）
     *
     * 管理员查看指定用户最近的悄悄话记录。
     * AJAX 请求返回 JSON，浏览器请求返回美观的 HTML 页面。
     *
     * @param  string  $username  目标用户名
     * @return JsonResponse|\Illuminate\View\View 私信记录
     */
    public function viewWhispers(Request $request, string $username): JsonResponse|\Illuminate\View\View
    {
        $admin = Auth::user();
        $superLevel = (int) Sysparam::getValue('superlevel', '100');

        if ($admin->user_level < $superLevel) {
            if ($request->expectsJson()) {
                return response()->json(['status' => 'error', 'message' => '仅站长可查看私信'], 403);
            }
            abort(403, '仅站长可查看私信');
        }

        // 查询最近 50 条悄悄话（发送或接收）
        $messages = Message::where('is_secret', true)
            ->where(function ($q) use ($username) {
                $q->where('from_user', $username)
                    ->orWhere('to_user', $username);
            })
            ->orderByDesc('id')
            ->limit(50)
            ->get(['id', 'from_user', 'to_user', 'content', 'sent_at']);

        // AJAX 请求返回 JSON（给名片弹窗用），浏览器请求返回 HTML 页面
        if ($request->expectsJson()) {
            return response()->json([
                'status' => 'success',
                'username' => $username,
                'messages' => $messages,
            ]);
        }

        return view('admin.whispers', [
            'username' => $username,
            'messages' => $messages,
        ]);
    }

    /**
     * 聊天室公屏讲话
     *
     * 拥有 room.public_broadcast 权限的职务可以发送全聊天室公告，
     * id=1 站长仍然拥有完整兜底权限。
     *
     * @param  Request  $request  请求对象，需包含 content, room_id
     * @return JsonResponse 操作结果
     */
    public function announce(Request $request): JsonResponse
    {
        $request->validate([
            'content' => 'required|string|max:500',
            'room_id' => 'required|integer',
        ]);

        $admin = Auth::user();
        if (! $this->positionPermissionService->hasPermission($admin, PositionPermissionRegistry::ROOM_PUBLIC_BROADCAST)) {
            return response()->json(['status' => 'error', 'message' => '当前职务无权发布公屏讲话'], 403);
        }

        $roomId = (int) $request->input('room_id');
        $roomAuthorization = $this->authorizeManagedRoom($roomId, $admin);
        if (! $roomAuthorization['ok']) {
            return response()->json(['status' => 'error', 'message' => $roomAuthorization['message']], 403);
        }

        $content = ChatContentSanitizer::htmlText($request->input('content'));

        // 按当前在职职务拼装发布者身份，避免继续显示为固定“站长公告”
        $publisherLabel = ChatContentSanitizer::htmlText($this->buildAnnouncementPublisherLabel($admin));
        $publisherUsername = ChatContentSanitizer::htmlText($admin->username);
        $msg = [
            'id' => $this->chatState->nextMessageId($roomId),
            'room_id' => $roomId,
            'from_user' => '系统公告',
            'to_user' => '大家',
            'content' => "📢 <b>{$publisherLabel}</b> <b>{$publisherUsername}</b> 发布公告：{$content}",
            'is_secret' => false,
            'font_color' => '#b91c1c',
            'action' => '',
            'sent_at' => now()->toDateTimeString(),
        ];
        $this->chatState->pushMessage($roomId, $msg);
        broadcast(new MessageSent($roomId, $msg));
        SaveMessageJob::dispatch($msg);

        return response()->json(['status' => 'success', 'message' => '公告已发送']);
    }

    /**
     * 生成公屏公告发布者身份标签。
     *
     * 普通在职用户按“部门+职务”显示；站长无在职职务时保持“站长”标识兜底。
     */
    private function buildAnnouncementPublisherLabel(User $user): string
    {
        return $this->buildOperatorIdentityLabel($user);
    }

    /**
     * 生成操作者的身份标签。
     *
     * 有在职职务时统一显示为“部门·职务”，无在职职务的 id=1 兜底显示“站长”。
     */
    private function buildOperatorIdentityLabel(User $user): string
    {
        $position = $user->activePosition?->position;

        if ($position) {
            $departmentName = (string) ($position->department?->name ?? '');

            return $departmentName ? "{$departmentName}·{$position->name}" : $position->name;
        }

        if ($user->id === 1) {
            return '站长';
        }

        return '管理员';
    }

    /**
     * 生成操作者在聊天室文案中的完整展示文本。
     */
    private function buildOperatorDisplayHtml(User $user): string
    {
        $identityLabel = e($this->buildOperatorIdentityLabel($user));
        $username = e($user->username);

        return "<b>{$identityLabel}</b> <b>{$username}</b>";
    }

    /**
     * 校验操作者是否可在指定房间执行聊天室管理命令。
     *
     * @return array{ok: bool, message: string, room?: Room}
     */
    private function authorizeManagedRoom(int $roomId, User $operator): array
    {
        $room = Room::query()->find($roomId);
        if (! $room) {
            return ['ok' => false, 'message' => '房间不存在'];
        }

        if (! $room->canUserEnter($operator)) {
            return ['ok' => false, 'message' => '无权进入该房间，不能执行管理命令'];
        }

        // 管理命令只能作用于操作者当前所在房间，防止手工 POST 跨房间操作。
        if (! $this->chatState->isUserInRoom($roomId, $operator->username)) {
            return ['ok' => false, 'message' => '请先进入该房间后再执行管理命令'];
        }

        return ['ok' => true, 'message' => '校验通过', 'room' => $room];
    }

    /**
     * 校验目标用户是否仍在线于当前房间。
     *
     * @return array{ok: bool, message: string}
     */
    private function authorizeTargetOnlineInRoom(int $roomId, string $targetUsername): array
    {
        if (! $this->chatState->isUserInRoom($roomId, $targetUsername)) {
            return ['ok' => false, 'message' => '目标用户不在当前房间，无法执行该操作'];
        }

        return ['ok' => true, 'message' => '校验通过'];
    }

    /**
     * 校验聊天室用户管理动作是否可执行。
     *
     * 规则：
     * - id=1 站长始终放行
     * - 其他人必须拥有对应职务权限
     * - 不能操作自己
     * - 不能处理 user_level 高于自己的用户
     * - 不能处理部门位阶或职务位阶高于自己的用户
     *
     * @return array{ok: bool, message: string, target?: User}
     */
    private function authorizeModerationAction(
        User $admin,
        string $targetUsername,
        string $permissionCode,
        string $actionLabel,
    ): array {
        if (! $this->positionPermissionService->hasPermission($admin, $permissionCode)) {
            return ['ok' => false, 'message' => "当前职务无权{$actionLabel}用户"];
        }

        if ($admin->username === $targetUsername) {
            return ['ok' => false, 'message' => '不能对自己执行该操作'];
        }

        $target = User::query()
            ->where('username', $targetUsername)
            ->with('activePosition.position.department')
            ->first();

        if (! $target) {
            return ['ok' => false, 'message' => '用户不存在'];
        }

        if (! $this->canModerateTargetByDutyRank($admin, $target)) {
            return ['ok' => false, 'message' => '不能处理职务高于自己的用户'];
        }

        if ($admin->id !== 1 && $target->user_level > $admin->user_level) {
            return ['ok' => false, 'message' => '不能处理等级高于自己的用户'];
        }

        return ['ok' => true, 'message' => '校验通过', 'target' => $target];
    }

    /**
     * 判断操作者是否可以按职务位阶处理目标用户。
     *
     * 规则：
     * - 先比较部门 rank
     * - 部门相同再比较职务 rank
     * - 对方没有在职职务时视为可处理
     * - 同职务平级允许操作，仅禁止处理更高职务
     */
    private function canModerateTargetByDutyRank(User $admin, User $target): bool
    {
        if ($admin->id === 1) {
            return true;
        }

        $adminPosition = $admin->activePosition?->load('position.department')->position;
        if (! $adminPosition) {
            return false;
        }

        $targetPosition = $target->activePosition?->position;
        if (! $targetPosition) {
            return true;
        }

        $adminDepartmentRank = (int) ($adminPosition->department?->rank ?? 0);
        $targetDepartmentRank = (int) ($targetPosition->department?->rank ?? 0);

        if ($adminDepartmentRank > $targetDepartmentRank) {
            return true;
        }

        if ($adminDepartmentRank < $targetDepartmentRank) {
            return false;
        }

        return (int) $adminPosition->rank >= (int) $targetPosition->rank;
    }

    /**
     * 管理员全员清屏
     *
     * 清除 Redis 中该房间的聊天记录缓存，并广播清屏事件通知所有用户前端清除消息。
     * 前端只清除普通消息，保留悄悄话。
     *
     * @param  Request  $request  请求对象，需包含 room_id
     * @return JsonResponse 操作结果
     */
    public function clearScreen(Request $request): JsonResponse
    {
        $request->validate([
            'room_id' => 'required|integer',
        ]);

        $admin = Auth::user();
        $roomId = (int) $request->input('room_id');
        // 改为按职务权限控制聊天室顶部“清屏”按钮。
        if (! $this->positionPermissionService->hasPermission($admin, PositionPermissionRegistry::ROOM_CLEAR_SCREEN)) {
            return response()->json(['status' => 'error', 'message' => '当前职务无权执行全员清屏'], 403);
        }

        $roomAuthorization = $this->authorizeManagedRoom($roomId, $admin);
        if (! $roomAuthorization['ok']) {
            return response()->json(['status' => 'error', 'message' => $roomAuthorization['message']], 403);
        }

        // 清除 Redis 中该房间的消息缓存
        $this->chatState->clearMessages($roomId);

        // 广播清屏事件
        broadcast(new \App\Events\ScreenCleared($roomId, $admin->username));

        return response()->json(['status' => 'success', 'message' => '已执行全员清屏']);
    }

    /**
     * 站长触发当前房间全员刷新页面。
     *
     * 仅允许 id=1 的站长使用，向当前聊天室在线用户广播刷新事件，
     * 适用于功能更新后强制让前端重新拉取最新页面状态。
     */
    public function refreshAll(Request $request): JsonResponse
    {
        $request->validate([
            'room_id' => 'required|integer',
            'reason' => 'nullable|string|max:100',
        ]);

        $admin = Auth::user();
        if ((int) $admin->id !== 1) {
            return response()->json(['status' => 'error', 'message' => '仅站长可执行全员刷新'], 403);
        }

        $roomId = (int) $request->input('room_id');
        $roomAuthorization = $this->authorizeManagedRoom($roomId, $admin);
        if (! $roomAuthorization['ok']) {
            return response()->json(['status' => 'error', 'message' => $roomAuthorization['message']], 403);
        }

        $reason = ChatContentSanitizer::htmlText($request->input('reason', ''));

        // 立即广播页面刷新指令，确保在线用户尽快拿到最新前端状态。
        broadcast(new BrowserRefreshRequested(
            roomId: $roomId,
            operator: $admin->username,
            reason: $reason,
        ));

        return response()->json([
            'status' => 'success',
            'message' => '已通知当前房间所有在线用户刷新页面',
        ]);
    }

    /**
     * 管理员触发全屏特效。
     *
     * 向房间内所有用户广播 EffectBroadcast 事件，前端收到后播放对应 Canvas 动画。
     * 仅拥有 room.fullscreen_effect 权限的职务可触发。
     *
     * @param  Request  $request  请求对象，需包含 room_id, type
     * @return JsonResponse 操作结果
     */
    public function effect(Request $request): JsonResponse
    {
        $request->validate([
            'room_id' => 'required|integer',
            'type' => ['required', 'string', Rule::in(EffectBroadcast::TYPES)],
        ]);

        $admin = Auth::user();
        $roomId = (int) $request->input('room_id');
        $type = $request->input('type');
        if (! $this->positionPermissionService->hasPermission($admin, PositionPermissionRegistry::ROOM_FULLSCREEN_EFFECT)) {
            return response()->json(['status' => 'error', 'message' => '当前职务无权触发特效'], 403);
        }

        $roomAuthorization = $this->authorizeManagedRoom($roomId, $admin);
        if (! $roomAuthorization['ok']) {
            return response()->json(['status' => 'error', 'message' => $roomAuthorization['message']], 403);
        }

        // 广播特效事件给房间内所有在线用户
        broadcast(new EffectBroadcast($roomId, $type, $admin->username));

        return response()->json(['status' => 'success', 'message' => "已触发特效：{$type}"]);
    }

    /**
     * 职务奖励金币（凭空发放，无需扣操作者余额）
     *
     * 三层限额校验：
     *  1. amount ≤ position.max_reward              （单次上限）
     *  2. 今日累计发放 + amount ≤ position.daily_reward_limit  （操作人单日累计上限）
     *  3. 今日对同一接收者发放次数 < position.recipient_daily_limit（同一接收者每日次数限）
     *
     * 成功后：
     *  - 通过 UserCurrencyService 给接收者增加金币
     *  - 写入 PositionAuthorityLog（action_type=reward，记录到履职记录）
     *  - 向房间发送悄悄话通知接收者
     *
     * @param  Request  $request  需包含 username, room_id, amount
     */
    public function reward(Request $request): JsonResponse
    {
        $request->validate([
            'username' => 'required|string',
            'room_id' => 'required|integer',
            'amount' => 'required|integer|min:1|max:999999999',
        ], [
            'amount.max' => '单次发放金币不能超过 999999999',
            'amount.min' => '发放金币至少为 1',
            'amount.integer' => '金币数量必须是整数',
            'amount.required' => '请输入要发放的金币数量',
        ]);

        $admin = Auth::user();
        $roomId = (int) $request->input('room_id');
        $amount = (int) $request->input('amount');
        $targetUsername = $request->input('username');
        $roomAuthorization = $this->authorizeManagedRoom($roomId, $admin);
        if (! $roomAuthorization['ok']) {
            return response()->json(['status' => 'error', 'message' => $roomAuthorization['message']], 403);
        }

        // 不能给自己发放
        if ($admin->username === $targetUsername) {
            return response()->json(['status' => 'error', 'message' => '不能给自己发放奖励'], 422);
        }

        // 目标用户必须存在
        $target = User::where('username', $targetUsername)->first();
        if (! $target) {
            return response()->json(['status' => 'error', 'message' => '用户不存在'], 404);
        }

        $targetAuthorization = $this->authorizeTargetOnlineInRoom($roomId, $targetUsername);
        if (! $targetAuthorization['ok']) {
            return response()->json(['status' => 'error', 'message' => $targetAuthorization['message']], 403);
        }

        // id=1 超级管理员：无需职务，无限额限制
        $isSuperAdmin = $admin->id === 1;
        $userPosition = null;
        $position = null;

        if (! $isSuperAdmin) {
            if (! $this->positionPermissionService->hasPermission($admin, PositionPermissionRegistry::ROOM_REWARD)) {
                return response()->json(['status' => 'error', 'message' => '当前职务无权发放奖励'], 403);
            }

            // ① 必须有在职职务
            $userPosition = $admin->activePosition;
            if (! $userPosition) {
                return response()->json(['status' => 'error', 'message' => '你当前没有在职职务，无权发放奖励'], 403);
            }

            $position = $userPosition->position;

            // 职务 max_reward = 0 表示禁止，null 表示不限，正整数表示有上限
            if ($position?->max_reward === 0) {
                return response()->json(['status' => 'error', 'message' => '你的职务未配置奖励权限'], 403);
            }

            // ② 单次上限校验（max_reward > 0 时才限制，null = 不限）
            if ($position->max_reward && $amount > $position->max_reward) {
                return response()->json([
                    'status' => 'error',
                    'message' => "单次奖励上限为 {$position->max_reward} 金币，请调整金额",
                ], 422);
            }

            // ③ 操作人单日累计上限校验
            if ($position->daily_reward_limit) {
                $todayTotal = PositionAuthorityLog::where('user_id', $admin->id)
                    ->where('action_type', 'reward')
                    ->whereDate('created_at', today())
                    ->sum('amount');

                if ($todayTotal + $amount > $position->daily_reward_limit) {
                    $remaining = max(0, $position->daily_reward_limit - $todayTotal);

                    return response()->json([
                        'status' => 'error',
                        'message' => "今日剩余可发放额度为 {$remaining} 金币，超出单日上限（{$position->daily_reward_limit}）",
                    ], 422);
                }
            }

            // ④ 职务级别：接收者每日次数上限
            if ($position->recipient_daily_limit) {
                $recipientCount = PositionAuthorityLog::where('target_user_id', $target->id)
                    ->where('action_type', 'reward')
                    ->whereDate('created_at', today())
                    ->count();

                if ($recipientCount >= $position->recipient_daily_limit) {
                    return response()->json([
                        'status' => 'error',
                        'message' => "{$targetUsername} 今日已由全体职务人员累计发放 {$recipientCount} 次奖励，已达每日上限（{$position->recipient_daily_limit}）",
                    ], 422);
                }
            }
        }

        // ⑤ 全局系统级别：每位用户单日最多接收奖励次数（所有操作人通用，含超管）
        $globalMax = (int) Sysparam::getValue('reward_recipient_daily_max', '0');
        if ($globalMax > 0) {
            $globalCount = PositionAuthorityLog::where('target_user_id', $target->id)
                ->where('action_type', 'reward')
                ->whereDate('created_at', today())
                ->count();

            if ($globalCount >= $globalMax) {
                return response()->json([
                    'status' => 'error',
                    'message' => "{$targetUsername} 今日已累计接收 {$globalCount} 次奖励，已达全局每日上限（{$globalMax}）",
                ], 422);
            }
        }

        // 发放金币（通过 UserCurrencyService 原子性更新 + 写流水）
        // 组合「部门 · 职务」显示名，超管特殊处理
        if ($isSuperAdmin) {
            $positionName = '超级管理员';
        } elseif ($position) {
            $deptName = $position->department?->name;
            $positionName = $deptName ? "{$deptName} · {$position->name}" : $position->name;
        } else {
            $positionName = '职务';
        }

        $this->currencyService->change(
            $target,
            'gold',
            $amount,
            CurrencySource::POSITION_REWARD,
            "{$admin->username}（{$positionName}）职务奖励",
            $roomId,
        );

        // 写履职记录（PositionAuthorityLog；超管无职务时 user_position_id 留 null）
        PositionAuthorityLog::create([
            'user_id' => $admin->id,
            'user_position_id' => $userPosition?->id,
            'action_type' => 'reward',
            'target_user_id' => $target->id,
            'amount' => $amount,
            'remark' => "发放奖励金币 {$amount} 枚给 {$targetUsername}",
        ]);

        // ① 聊天室公开公告（所有在场用户可见）
        $publicMsg = [
            'id' => $this->chatState->nextMessageId($roomId),
            'room_id' => $roomId,
            'from_user' => '系统公告',
            'to_user' => '',
            'content' => "💰 <b>{$admin->username}</b>（{$positionName}）向 <b>{$targetUsername}</b> 发放了 <b>{$amount}</b> 枚奖励金币！",
            'is_secret' => false,
            'font_color' => '#d97706',
            'action' => '',
            'sent_at' => now()->toDateTimeString(),
        ];
        $this->chatState->pushMessage($roomId, $publicMsg);
        broadcast(new MessageSent($roomId, $publicMsg));
        SaveMessageJob::dispatch($publicMsg);

        // ② 接收者私信（含 toast_notification 触发右下角小卡片）
        $freshJjb = $target->fresh()->jjb;
        $msg = [
            'id' => $this->chatState->nextMessageId($roomId),
            'room_id' => $roomId,
            'from_user' => '系统',
            'to_user' => $targetUsername,
            'content' => "🎁 <b>{$admin->username}</b>（{$positionName}）向你发放了 <b>{$amount}</b> 枚金币奖励！当前金币：{$freshJjb} 枚。",
            'is_secret' => true,
            'font_color' => '#d97706',
            'action' => '',
            'sent_at' => now()->toDateTimeString(),
            // 前端 toast-notification 组件识别此字段，弹出右下角通知卡片
            'toast_notification' => [
                'title' => '💰 奖励金币到账',
                'message' => "<b>{$admin->username}</b>（{$positionName}）向你发放了 <b>{$amount}</b> 枚金币！",
                'icon' => '💰',
                'color' => '#f59e0b',
                'duration' => 8000,
            ],
        ];
        $this->chatState->pushMessage($roomId, $msg);
        broadcast(new MessageSent($roomId, $msg));
        SaveMessageJob::dispatch($msg);

        return response()->json([
            'status' => 'success',
            'message' => "已向 {$targetUsername} 发放 {$amount} 金币奖励 🎉",
        ]);
    }

    /**
     * 查询当前操作人的奖励额度信息（供发放弹窗展示）
     *
     * 返回字段：
     *   - max_once:        单次上限（null = 不限）
     *   - daily_limit:     单日发放总额上限（null = 不限）
     *   - today_sent:      今日已发放总额
     *   - daily_remaining: 今日剩余可发放额度（null = 不限）
     */
    public function rewardQuota(): \Illuminate\Http\JsonResponse
    {
        $admin = Auth::user();
        $isSuperAdmin = $admin->id === 1;

        // 最近 10 条本人发放记录（含目标用户名）
        $recent = PositionAuthorityLog::with('targetUser:id,username')
            ->where('user_id', $admin->id)
            ->where('action_type', 'reward')
            ->latest()
            ->limit(10)
            ->get()
            ->map(fn ($log) => [
                'target' => $log->targetUser?->username ?? '未知',
                'amount' => $log->amount,
                'created_at' => $log->created_at?->format('m-d H:i'),
            ]);

        if ($isSuperAdmin) {
            return response()->json([
                'max_once' => null,
                'daily_limit' => null,
                'today_sent' => (int) PositionAuthorityLog::where('user_id', $admin->id)
                    ->where('action_type', 'reward')
                    ->whereDate('created_at', today())
                    ->sum('amount'),
                'daily_remaining' => null,
                'recent_rewards' => $recent,
            ]);
        }

        $position = $admin->activePosition?->position;
        if (! $position) {
            return response()->json([
                'max_once' => 0,
                'daily_limit' => null,
                'today_sent' => 0,
                'daily_remaining' => null,
                'recent_rewards' => $recent,
            ]);
        }

        // 今日已发放总额
        $todaySent = (int) PositionAuthorityLog::where('user_id', $admin->id)
            ->where('action_type', 'reward')
            ->whereDate('created_at', today())
            ->sum('amount');

        $dailyLimit = $position->daily_reward_limit;
        $remaining = $dailyLimit !== null ? max(0, $dailyLimit - $todaySent) : null;

        return response()->json([
            'max_once' => $position->max_reward,
            'daily_limit' => $dailyLimit,
            'today_sent' => $todaySent,
            'daily_remaining' => $remaining,
            'recent_rewards' => $recent,
        ]);
    }

    /**
     * 向目标用户补发一条系统私聊消息，并附带右下角 toast 配置。
     */
    private function pushTargetToastMessage(
        int $roomId,
        string $targetUsername,
        string $content,
        string $title,
        string $toastMessage,
        string $color,
        string $icon,
    ): void {
        $msg = [
            'id' => $this->chatState->nextMessageId($roomId),
            'room_id' => $roomId,
            'from_user' => '系统',
            'to_user' => $targetUsername,
            'content' => $content,
            'is_secret' => true,
            'font_color' => $color,
            'action' => '',
            'sent_at' => now()->toDateTimeString(),
            // 复用现有聊天 toast 机制，在右下角弹出操作结果提示。
            'toast_notification' => [
                'title' => $title,
                'message' => $toastMessage,
                'icon' => $icon,
                'color' => $color,
                'duration' => 10000,
            ],
        ];

        $this->chatState->pushMessage($roomId, $msg);
        broadcast(new MessageSent($roomId, $msg));
        SaveMessageJob::dispatch($msg);
    }
}
