<?php

/**
 * 文件功能：聊天全局状态中心 (替代旧版 ASP中的 Application 全局对象)
 * 依赖 Redis 内存存取实现在线人员列表、发言记录的高并发读写。
 *
 * @author   ChatRoom Laravel
 *
 * @version  1.0.0
 */

namespace App\Services;

use App\Models\SysParam;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Redis;

class ChatStateService
{
    /**
     * 将用户加入到指定房间的在线列表中。
     *
     * @param  int  $roomId  房间ID
     * @param  string  $username  用户名
     * @param  array  $info  用户详细信息 (头像、等级等)
     */
    public function userJoin(int $roomId, string $username, array $info): void
    {
        $key = "room:{$roomId}:users";
        Redis::hset($key, $username, json_encode($info, JSON_UNESCAPED_UNICODE));
        // 写入活跃标记（90秒 TTL，心跳每次刷新；超出则被认定为掉线）
        Redis::setex("room:{$roomId}:alive:{$username}", 90, 1);
    }

    /**
     * 将用户从指定房间的在线列表中移除。
     *
     * @param  int  $roomId  房间ID
     * @param  string  $username  用户名
     */
    public function userLeave(int $roomId, string $username): void
    {
        $key = "room:{$roomId}:users";
        Redis::hdel($key, $username);
        // 同时删除活跃标记
        Redis::del("room:{$roomId}:alive:{$username}");
    }

    /**
     * 判断用户是否已经在某个房间的在线列表中
     */
    public function isUserInRoom(int $roomId, string $username): bool
    {
        $key = "room:{$roomId}:users";

        return Redis::hexists($key, $username);
    }

    /**
     * 刷新用户心跳活跃标记（心跳接口调用）。
     *
     * @param  int  $roomId  房间ID
     * @param  string  $username  用户名
     */
    public function refreshAlive(int $roomId, string $username): void
    {
        Redis::setex("room:{$roomId}:alive:{$username}", 90, 1);
    }

    /**
     * 查找用户当前所在的所有房间 ID
     *
     * 扫描 Redis 中所有 room:*:users 的哈希表，检查用户是否在其中。
     *
     * @param  string  $username  用户名
     * @return array<int> 房间 ID 数组
     */
    public function getUserRooms(string $username): array
    {
        $rooms = [];
        $prefix = config('database.redis.options.prefix', '');
        $cursor = '0';
        do {
            // scan 带前缀通配，返回的 key 也带前缀
            [$cursor, $keys] = Redis::scan($cursor, ['match' => $prefix.'room:*:users', 'count' => 100]);
            foreach ($keys ?? [] as $fullKey) {
                // 去掉前缀得到 Laravel Redis Facade 认识的短少 key
                $shortKey = $prefix ? ltrim(substr($fullKey, strlen($prefix)), '') : $fullKey;
                if (Redis::hexists($shortKey, $username)) {
                    preg_match('/room:(\d+):users/', $shortKey, $m);
                    if (isset($m[1])) {
                        $rooms[] = (int) $m[1];
                    }
                }
            }
        } while ($cursor !== '0');

        return $rooms;
    }

    /**
     * 获取指定房间的所有在线用户列表。
     *
     * @param  int  $roomId  房间ID
     */
    public function getRoomUsers(int $roomId): array
    {
        $key = "room:{$roomId}:users";
        $users = Redis::hgetall($key);

        $result = [];
        foreach ($users as $username => $jsonInfo) {
            // 过滤掌心跳已超出活跃窗口的用户（掉线超 90秒）
            if (! Redis::exists("room:{$roomId}:alive:{$username}")) {
                // 懒清理：顺手将僵尸记录从 hash 中移除
                Redis::hdel($key, $username);

                continue;
            }
            $result[$username] = json_decode($jsonInfo, true);
        }

        return $result;
    }

    /**
     * 扫描 Redis，返回当前所有有在线用户的房间 ID 数组（用于全局广播）。
     *
     * @return array<int>
     */
    public function getAllActiveRoomIds(): array
    {
        $roomIds = [];
        $prefix = config('database.redis.options.prefix', '');
        $cursor = '0';
        do {
            // scan 带前缀通配，返回的 key 也带前缀
            [$cursor, $keys] = Redis::scan($cursor, ['match' => $prefix.'room:*:users', 'count' => 100]);
            foreach ($keys ?? [] as $fullKey) {
                $shortKey = $prefix ? substr($fullKey, strlen($prefix)) : $fullKey;
                // 只有 hash 非空（有在线用户）才算活跃房间
                if (Redis::hlen($shortKey) > 0) {
                    preg_match('/room:(\d+):users/', $shortKey, $m);
                    if (isset($m[1])) {
                        $roomIds[] = (int) $m[1];
                    }
                }
            }
        } while ($cursor !== '0');

        return array_unique($roomIds);
    }

    /**
     * 获取全局所有在线用户名（跨所有房间，去重）。
     *
     * 遍历所有活跃房间，合并用户名后去重，
     * 用于好友面板在线状态标记。
     *
     * @return array<string>
     */
    public function getAllOnlineUsernames(): array
    {
        $usernames = [];
        foreach ($this->getAllActiveRoomIds() as $roomId) {
            $key = "room:{$roomId}:users";
            $users = Redis::hkeys($key); // 只取 key（用户名），不取 value
            foreach ($users as $username) {
                $usernames[] = $username;
            }
        }

        return array_unique($usernames);
    }

    /**
     * 将一条新发言推入 Redis 列表，并限制最大保留数量，防止内存泄漏。
     *
     * @param  int  $roomId  房间ID
     * @param  array  $message  发言数据包
     * @param  int  $maxKeep  最大保留条数
     */
    public function pushMessage(int $roomId, array $message, int $maxKeep = 100): void
    {
        $key = "room:{$roomId}:messages";
        Redis::rpush($key, json_encode($message, JSON_UNESCAPED_UNICODE));

        // 仅保留最新的 $maxKeep 条，旧的自动截断弹弃 (-$maxKeep 到 -1 的区间保留)
        Redis::ltrim($key, -$maxKeep, -1);
    }

    /**
     * 清除指定房间的所有消息缓存（管理员全员清屏）。
     *
     * @param  int  $roomId  房间ID
     */
    public function clearMessages(int $roomId): void
    {
        $key = "room:{$roomId}:messages";
        Redis::del($key);
    }

    /**
     * 清除指定房间内，关于某个用户的旧欢迎消息（支持普通人、管理员、新人）。
     * 保证聊天记录里只保留最新的一条，解决频繁进出造成的刷屏问题。
     *
     * @param  int  $roomId  房间ID
     * @param  string  $username  用户名
     */
    public function removeOldWelcomeMessages(int $roomId, string $username): void
    {
        $key = "room:{$roomId}:messages";
        $messages = Redis::lrange($key, 0, -1);

        if (empty($messages)) {
            return;
        }

        $filtered = [];

        foreach ($messages as $msgJson) {
            $msg = json_decode($msgJson, true);
            // 只要消息里带了 welcome_user 且等于当前用户，就抛弃这条旧的
            if ($msg && isset($msg['welcome_user']) && $msg['welcome_user'] === $username) {
                continue;
            }
            $filtered[] = $msgJson;
        }

        // 重新写回 Redis（如果发生了过滤）
        if (count($filtered) !== count($messages)) {
            Redis::del($key);
            if (! empty($filtered)) {
                Redis::rpush($key, ...$filtered);
            }
        }
    }

    /**
     * 获取指定房间的新发言记录。
     * 在高频长轮询或前端断线重连拉取时使用。
     *
     * @param  int  $roomId  房间ID
     * @param  int  $lastId  客户端收到的最后一条发言的ID
     */
    public function getNewMessages(int $roomId, int $lastId): array
    {
        $key = "room:{$roomId}:messages";
        $messages = Redis::lrange($key, 0, -1); // 获取当前缓存的全部

        $newMessages = [];
        foreach ($messages as $msgJson) {
            $msg = json_decode($msgJson, true);
            if (isset($msg['id']) && $msg['id'] > $lastId) {
                $newMessages[] = $msg;
            }
        }

        return $newMessages;
    }

    /**
     * 分布式发号器：为房间内的新消息生成绝对递增的 ID。
     * 解决了 MySQL 自增在极致并发下依赖读锁的问题。
     *
     * @param  int  $roomId  房间ID
     */
    public function nextMessageId(int $roomId): int
    {
        $key = "room:{$roomId}:message_seq";

        return Redis::incr($key);
    }

    /**
     * 读取系统参数并设置合理的缓存。
     * 替代每次都去 MySQL query 的性能损耗。
     *
     * @param  string  $alias  别名
     */
    public function getSysParam(string $alias): ?string
    {
        return Cache::remember("sys_param:{$alias}", 60, function () use ($alias) {
            return SysParam::where('alias', $alias)->value('body');
        });
    }

    /**
     * 更新系统参数并刷新 Cache 缓存。
     *
     * @param  string  $alias  别名
     * @param  mixed  $value  参数值
     */
    public function setSysParam(string $alias, mixed $value): void
    {
        Cache::put("sys_param:{$alias}", $value);
    }

    /**
     * 提供一个分布式的 Redis 互斥锁包围。
     * 防止并发抢占资源（如同时创建重名房间，同时修改某一敏感属性）。
     *
     * @param  string  $key  锁名称
     * @param  callable  $callback  获得锁后执行的闭包
     * @param  int  $timeout  锁超时时间（秒）
     * @return mixed
     */
    public function withLock(string $key, callable $callback, int $timeout = 5)
    {
        $lockKey = "lock:{$key}";
        // 尝试获取锁，set nx ex
        $isLocked = Redis::set($lockKey, 1, 'EX', $timeout, 'NX');

        if (! $isLocked) {
            // 获取锁失败，业务可自行决定抛异常或重试
            throw new \Exception("The lock {$key} is currently held by another process.");
        }

        try {
            return $callback();
        } finally {
            Redis::del($lockKey);
        }
    }
}
