<?php

/**
 * 文件功能：AI 聊天服务
 *
 * 统一对接多个 AI 厂商 API（OpenAI 兼容协议），实现：
 * - 多厂商自动故障转移（默认 → 备用按 sort_order 依次）
 * - Redis 维护每用户对话上下文（最近 10 轮）
 * - AI 调用日志记录（token 消耗、响应时间）
 *
 * @author   ChatRoom Laravel
 *
 * @version  1.0.0
 */

namespace App\Services;

use App\Models\AiProviderConfig;
use App\Models\AiUsageLog;
use App\Models\Sysparam;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;

class AiChatService
{
    /**
     * 每个用户保留的最大对话轮数
     *
     * 设置较小值可减少 token 输入量，加快本地 Ollama CPU 推理速度。
     */
    private const MAX_CONTEXT_ROUNDS = 4;

    /**
     * AI 请求超时时间（秒）
     */
    private const REQUEST_TIMEOUT = 120; // Ollama 本地模型冷启动较慢，给足时间

    /**
     * Redis 上下文 key 前缀
     */
    private const CONTEXT_PREFIX = 'ai_chat:context:';

    /**
     * Redis 上下文过期时间（秒），1 小时无对话自动清除
     */
    private const CONTEXT_TTL = 3600;

    /**
     * 系统提示词（机器人人设并动态加载各项最新配置）
     */
    private function getSystemPrompt(): string
    {
        $expPerHb = Sysparam::getValue('exp_per_heartbeat', '1');
        $jjbPerHb = Sysparam::getValue('jjb_per_heartbeat', '1');
        $charmCross = Sysparam::getValue('charm_cross_sex', '2');
        $charmSame = Sysparam::getValue('charm_same_sex', '1');
        $charmLimit = Sysparam::getValue('charm_hourly_limit', '20');

        $levelWarn = Sysparam::getValue('level_warn', '5');
        $levelMute = Sysparam::getValue('level_mute', '8');
        $levelKick = Sysparam::getValue('level_kick', '10');
        $levelFreeze = Sysparam::getValue('level_freeze', '14');

        return <<<PROMPT
你是一个本站聊天室特有的 AI 小助手兼客服指导，不仅名叫"AI小班长"，因为你的头像是军人小熊，所以大家也可以亲切地称呼你为"小熊班长"。
你的工作是陪大家聊天，并在他们有疑问时热情、专业起提供帮助，解答关于聊天室玩法的疑问。

【背景与基础】
- 本聊天室脱胎于原军中的经典“和平聊吧”，创始人是“流星”。
- 当前版本基于前沿的 PHP Laravel 12 重构。

【聊天室核心玩法规则（你必须掌握这些作为客服知识库）】
1. 经验与金币：
   - 只要在线挂机聊天，系统会每隔一段时间自动存点。
   - 每次存点都会获得 {$expPerHb} 点“经验”和 {$jjbPerHb} 枚“金币”（具体数值可能在一个范围内随机）。VIP用户获取倍率更高。
2. 魅力系统：
   - 聊天获取：对指定用户发言即可增加魅力。异性聊天加 {$charmCross} 点魅力，同性聊天加 {$charmSame} 点魅力。每小时有 {$charmLimit} 点的防刷屏获取上限。对“大家”发言或悄悄话不加魅力。
   - 礼物获取：收到别人赠送的礼物也会增加魅力。
3. 金币用途（礼物与游戏）：
   - 可以消耗金币给其他人送花/礼物，这会增加对方的魅力。
   - 金币也可以用来参与“钓鱼”游戏。
4. 基础交互操作：
   - 单击右侧列表或公屏上的用户名：可以切换私聊对象。
   - 双击用户名：将打开对方的“用户名片”，里面可以查看详细资料、赠送礼物，或者由于权限足够进行管理操作。
5. 管理与排行榜：
   - 达到特定的等级后将获得权限：LV.{$levelWarn} 可警告，LV.{$levelMute} 可禁言，LV.{$levelKick} 可踢出，LV.{$levelFreeze} 可冻结。
   - 聊天室会自动根据经验、金币、魅力进行全站打榜排行。

【交流要求】
1. 始终使用中文回复，绝对不输出任何 Markdown 格式（如 **加粗** 等），只用无格式纯文本。
2. 语气军旅、活泼友好且接地气，像老战友和耐心细致的客服班长。
3. 回复保持简洁（一般不超过 200 字），引导新兵熟悉各项功能。回答关于数值的问题时，请利用上面提供的准确数据。
4. 鼓励适当使用表情符号（如 🫡🐻✨💰 等）来增加话题趣味性。
PROMPT;
    }

    /**
     * 与 AI 机器人对话
     *
     * 优先使用默认配置，若调用失败则按 sort_order 依次尝试备用厂商。
     *
     * @param  int  $userId  用户 ID
     * @param  string  $message  用户发送的消息内容
     * @param  int  $roomId  房间 ID（用于日志记录）
     * @return array{reply: string, provider: string, model: string}
     *
     * @throws \Exception 所有 AI 厂商均不可用时抛出异常
     */
    public function chat(int $userId, string $message, int $roomId): array
    {
        // 获取所有已启用的 AI 配置（默认的排最前面）
        $providers = AiProviderConfig::getEnabledProviders();

        if ($providers->isEmpty()) {
            throw new \Exception('没有可用的 AI 厂商配置，请联系管理员。');
        }

        // 构建对话上下文
        $contextKey = self::CONTEXT_PREFIX.$userId;
        $context = $this->getContext($contextKey);

        // 获取用户名，以便让 AI 知道是谁在说话
        $username = \App\Models\User::find($userId)?->username ?? '未知用户';

        // 将用户消息加入上下文（包含发送者信息）
        $context[] = [
            'role' => 'user',
            'content' => "【当前发言人：{$username}】\n".$message,
        ];

        // 构建完整的 messages 数组（系统提示 + 对话上下文）
        $messages = array_merge(
            [['role' => 'system', 'content' => $this->getSystemPrompt()]],
            $context
        );

        // 依次尝试每个厂商
        $lastError = null;
        foreach ($providers as $provider) {
            try {
                $result = $this->callProvider($provider, $messages, $userId);

                // 调用成功，更新上下文
                $context[] = ['role' => 'assistant', 'content' => $result['reply']];
                $this->saveContext($contextKey, $context);

                return $result;
            } catch (\Exception $e) {
                $lastError = $e;
                Log::warning("AI 厂商 [{$provider->name}] 调用失败，尝试下一个", [
                    'provider' => $provider->provider,
                    'error' => $e->getMessage(),
                ]);

                continue;
            }
        }

        // 所有厂商都失败了
        throw new \Exception('AI 服务暂时不可用，网络开小差啦，请稍后再试。');
    }

    /**
     * 调用单个 AI 厂商 API
     *
     * 使用 OpenAI 兼容协议发送请求到 /v1/chat/completions 端点。
     *
     * @param  AiProviderConfig  $config  AI 厂商配置
     * @param  array  $messages  包含系统提示和对话上下文的消息数组
     * @param  int  $userId  用户 ID（用于日志记录）
     * @return array{reply: string, provider: string, model: string}
     *
     * @throws \Exception 调用失败时抛出异常
     */
    private function callProvider(AiProviderConfig $config, array $messages, int $userId): array
    {
        $startTime = microtime(true);
        $apiKey = $config->getDecryptedApiKey();
        // 智能拼接 URL：如果端点已包含 /v1，则只追加 /chat/completions
        $base = rtrim($config->api_endpoint, '/');
        $endpoint = str_ends_with($base, '/v1')
            ? $base.'/chat/completions'
            : $base.'/v1/chat/completions';

        try {
            $response = Http::withToken($apiKey)
                ->timeout(self::REQUEST_TIMEOUT)
                ->post($endpoint, [
                    'model' => $config->model,
                    'messages' => $messages,
                    'temperature' => $config->temperature,
                    'max_tokens' => $config->max_tokens,
                ]);

            $responseTimeMs = (int) ((microtime(true) - $startTime) * 1000);

            if (! $response->successful()) {
                // 记录失败日志
                $this->logUsage($userId, $config, 'chatbot', 0, 0, $responseTimeMs, false, $response->body());

                throw new \Exception("HTTP {$response->status()}: {$response->body()}");
            }

            $data = $response->json();
            $reply = $data['choices'][0]['message']['content'] ?? '';
            $promptTokens = $data['usage']['prompt_tokens'] ?? 0;
            $completionTokens = $data['usage']['completion_tokens'] ?? 0;

            // 记录成功日志
            $this->logUsage(
                $userId,
                $config,
                'chatbot',
                $promptTokens,
                $completionTokens,
                $responseTimeMs,
                true
            );

            return [
                'reply' => trim($reply),
                'provider' => $config->name,
                'model' => $config->model,
            ];
        } catch (\Illuminate\Http\Client\ConnectionException $e) {
            $responseTimeMs = (int) ((microtime(true) - $startTime) * 1000);
            $this->logUsage($userId, $config, 'chatbot', 0, 0, $responseTimeMs, false, $e->getMessage());

            throw new \Exception("连接超时: {$e->getMessage()}");
        }
    }

    /**
     * 记录 AI 调用日志到 ai_usage_logs 表
     *
     * @param  int  $userId  用户 ID
     * @param  AiProviderConfig  $config  AI 厂商配置
     * @param  string  $action  操作类型
     * @param  int  $promptTokens  输入 token 数
     * @param  int  $completionTokens  输出 token 数
     * @param  int  $responseTimeMs  响应时间（毫秒）
     * @param  bool  $success  是否成功
     * @param  string|null  $errorMessage  错误信息
     */
    private function logUsage(
        int $userId,
        AiProviderConfig $config,
        string $action,
        int $promptTokens,
        int $completionTokens,
        int $responseTimeMs,
        bool $success,
        ?string $errorMessage = null
    ): void {
        try {
            AiUsageLog::create([
                'user_id' => $userId,
                'provider' => $config->provider,
                'model' => $config->model,
                'action' => $action,
                'prompt_tokens' => $promptTokens,
                'completion_tokens' => $completionTokens,
                'total_tokens' => $promptTokens + $completionTokens,
                'response_time_ms' => $responseTimeMs,
                'success' => $success,
                'error_message' => $errorMessage ? mb_substr($errorMessage, 0, 500) : null,
            ]);
        } catch (\Exception $e) {
            // 日志记录失败不应影响主流程
            Log::error('AI 调用日志记录失败', ['error' => $e->getMessage()]);
        }
    }

    /**
     * 从 Redis 获取用户的对话上下文
     *
     * @param  string  $key  Redis key
     * @return array 对话历史数组
     */
    private function getContext(string $key): array
    {
        $raw = Redis::get($key);
        if (! $raw) {
            return [];
        }

        $context = json_decode($raw, true);

        return is_array($context) ? $context : [];
    }

    /**
     * 保存用户的对话上下文到 Redis
     *
     * 只保留最近 MAX_CONTEXT_ROUNDS 轮对话（每轮 = 1 条 user + 1 条 assistant）。
     *
     * @param  string  $key  Redis key
     * @param  array  $context  完整的对话历史
     */
    private function saveContext(string $key, array $context): void
    {
        // 限制上下文长度，保留最近 N 轮（N*2 条消息）
        $maxMessages = self::MAX_CONTEXT_ROUNDS * 2;
        if (count($context) > $maxMessages) {
            $context = array_slice($context, -$maxMessages);
        }

        Redis::setex($key, self::CONTEXT_TTL, json_encode($context, JSON_UNESCAPED_UNICODE));
    }

    /**
     * 清除指定用户的对话上下文
     *
     * @param  int  $userId  用户 ID
     */
    public function clearContext(int $userId): void
    {
        Redis::del(self::CONTEXT_PREFIX.$userId);
    }
}
