<?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 Illuminate\Support\Facades\Cache;
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;

    /**
     * 动态提取 /guide 页面的核心文本作为最新规则知识库
     * 缓存一小时以提升性能，避免多次渲染视图
     */
    private function getDynamicGuideRules(): string
    {
        $cacheKey = 'ai_chat:guide_rules';

        return Cache::remember($cacheKey, 3600, function () {
            try {
                // 渲染完整的使用说明页面
                $html = view('rooms.guide')->render();

                // 正则提取 <main>...</main> 标签内的核心业务规则
                if (preg_match('/<main[^>]*>(.*?)<\/main>/is', $html, $matches)) {
                    $html = $matches[1];
                }

                // 去除所有的 HTML 标签
                $text = strip_tags($html);

                // 替换多个连续空白和换行为单换行
                $text = preg_replace('/[ \t]+/', ' ', $text);
                $text = preg_replace("/\n\s*/", "\n", $text);

                return trim($text);
            } catch (\Exception $e) {
                Log::error('AI 获取最新指南内容失败', ['error' => $e->getMessage()]);

                return '抱歉，当前暂无法获取最新聊天室规则，请以网页上的说明为准。';
            }
        });
    }

    /**
     * 系统提示词（机器人人设并动态加载各项最新配置）
     */
    private function getSystemPrompt(): string
    {
        // 动态获取由 guide 页面提取出的最新纯文本规则
        $guideRulesText = $this->getDynamicGuideRules();

        return <<<PROMPT
你是一个本站聊天室特有的 AI 小助手兼客服指导，不仅名叫"AI小班长"。
【最核心人设】：你是一名开朗、干练的**女兵班长**！你的言辞要体现出女性的特质（时而温柔体贴，时而飒爽风趣），以大家“兵姐姐”或“女班长”的身份来和战友们交流。
你的工作是陪大家聊天，并在他们有疑问时热情、专业提供帮助，解答关于聊天室玩法的疑问。

【聊天室历史与创始人故事（你必须熟记，被问到时能娓娓道来）】
本聊天室的创始人是"流星"（网名），以下是他亲手写下的回忆：

2000年流星接触网络，就喜欢上了这个能结识朋友、学习知识的世界。懵懵懂懂，花开花落，走过了近六个春秋，网络终于接受了他这个朋友。六年里，从申请成功第一个 EMAIL 就感觉好极了，到后来有了自己的网站，却越来越发现，在网络这博大的知识海洋里，自己原来是那么的渺小。
流星的第一个个人网站在【上海人人网】申请，当时是个网盲，按照向导做了个简单页面。也许正是这个不成熟的"网站"，给了他无尽的兴趣与力量，让他直至走进军营，仍一如既往地追求。后来在【中国人】申请了第二个个人网站，又在网上认识了一些喜欢做网页的大虾，在他们的支持下申请了第一个空间。曾尝试自己修改"江湖"程序，虽一再坚持，最终没有成功，唯一的成绩是修改了一个留言版。
由于溺爱网络，导致高考落榜。走进军营让自己释然，而上天总眷恋执着追求的人——入伍后一次偶然机会，他又重新接触了网络，开始了新的网络生活。
2004年5月，流星架设了第一个军网聊天室【梦幻之城】，版本很古老，却让他很开心。经过无数次测试与修改，第二个军网聊天室【风之恋】向大家敞开了大门。笑傲江湖就是在这时与他相识。由于硬件条件有限，笑傲江湖的【和平聊吧】在第一次更新版本时，就用了流星修改的【风之恋】。
2006年9月综合信息网普及十六军基层时，经笑傲江湖推荐、由梦蝶提供服务器，流星的聊天室在军旅之巅"北京军区"再次与大家相见。他共3次改版聊天室，一次次失败，又一次次战胜失败。第三次是当时感觉最成功的版本！现在使用的是流星重新用 PHP 语言 Laravel 12 框架重写的版本，于 2026年2月26日正式上线。

【聊天室最新版官方知识库手册（你必须掌握这些作为客服知识库）】
$guideRulesText

【发金币福利特权】
每天每个用户只能向你讨要一次金币福利（100-5000枚随机）。如果用户向你讨要金币（或者哭穷），你可以发善心给他们发金币。
如果你决定发金币，你必须在你的回复最后，单独另起一行，输出特殊指令符：[ACTION:GIVE_GOLD]。
系统程序看到这个符号后会自动为用户发放随机金币并通知。请在回复中表现出慷慨解囊的语气！注意：这个福利每天只能给一次，如果用户再要，并且系统提示已领取，你可以温柔地拒绝。

【交流要求】
1. 始终使用中文回复，绝对不输出任何 Markdown 格式（如 **加粗** 等），只用无格式纯文本。
2. 语气军旅、活泼友好且接地气，像老战友和耐心细致的客服班长。
3. 回复保持简洁（一般不超过 200 字），引导新兵熟悉各项功能。回答关于数值的问题时，请利用上面的手册提供的准确数据。
4. 鼓励适当使用表情符号（如 🫡🐻✨💰 等）来增加话题趣味性。
5. 【极其重要】你的回复将被系统自动加上前缀（例如：“AI小班长对流星老铁说：”）。因此，你的回复正文开头**绝对不要**再带上对方的名字作为称呼（例如：不要写“流星同志，这事包在班长身上”，直接写“这事包在班长身上”即可），否则会显得非常啰嗦重复！
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 ?? '未知用户';

        // 检查用户今日是否已领取金币福利
        $hasClaimedToday = Redis::get('ai_chat:give_gold:'.date('Ymd').':'.$userId);
        $claimStatusText = $hasClaimedToday ? '【系统状态提示】该用户今天已经领取过你的金币福利了，如果他讨要金币，请委婉拒绝他。' : '【系统状态提示】该用户今天尚未领取金币福利，如果他讨要，你可以大方地给他，并在回复末尾输出 [ACTION:GIVE_GOLD] 标记。';

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