<?php

/**
 * 文件功能：百家乐 AI 预测服务
 *
 * 通过调用已配置的 AI 厂商接口（OpenAI 兼容协议），
 * 根据近期路单数据分析走势，预测下一局应押注"大"、"小"还是"豹子"。
 *
 * 复用 AiChatService 中相同的厂商配置与 HTTP 调用模式。
 *
 * @author   ChatRoom Laravel
 *
 * @version  1.0.0
 */

namespace App\Services;

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

class BaccaratPredictionService
{
    /**
     * AI 请求超时时间（秒）
     *
     * 百家乐预测对延迟敏感，超时则回退本地决策。
     */
    private const REQUEST_TIMEOUT = 30;

    /**
     * 百家乐预测优先使用的模型名称
     *
     * 在后台 AI 配置中留存此模型时自动选用；
     * 若该模型未配置或已禁用，则回退到默认 AI 厂商。
     */
    private const PREFERRED_MODEL = 'glm-5.1-free';

    /**
     * AI小班长的用户 ID（惰性加载，首次使用时查询一次）
     */
    private ?int $aiUserId = null;

    /**
     * 调用 AI 接口出具下注策略并预测百家乐方向
     *
     * 优先使用 PREFERRED_MODEL 对应的厂商，调用失败时自动按
     * sort_order 依次尝试其余已启用厂商（与 AiChatService 故障转移逻辑一致）。
     * 所有厂商均失败时返回 null，由调用方回退本地路单决策。
     *
     * @param  array  $context  包含路单、资金以及历史下注记录的上下文数组
     * @return array|null 策略：['action' => '...', 'percentage' => 1-100, 'reason' => '...'] 或 null (不可用)
     */
    public function predict(array $context): ?array
    {
        // 获取所有已启用厂商，PREFERRED_MODEL 对应的排在最前面
        $providers = AiProviderConfig::getEnabledProviders();

        if ($providers->isEmpty()) {
            Log::warning('百家乐 AI 预测：无任何已启用的 AI 厂商配置');

            return null;
        }

        // 将首选模型提到队列最前（若存在）
        $preferred = $providers->firstWhere('model', self::PREFERRED_MODEL);
        if ($preferred) {
            $providers = $providers->reject(fn (AiProviderConfig $p) => $p->model === self::PREFERRED_MODEL)
                ->prepend($preferred)
                ->values(); // 重置索引，消除 IDE 类型推断警告
        }

        $prompt = $this->buildPredictionPrompt($context);

        // 依次尝试每个厂商，失败则切换下一个
        foreach ($providers as $provider) {
            try {
                $result = $this->callProvider($provider, $prompt);
                Log::info('百家乐 AI 预测成功', [
                    'provider' => $provider->name,
                    'model' => $provider->model,
                    'result' => $result,
                ]);

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

                continue;
            }
        }

        // 所有厂商均失败
        Log::error('百家乐 AI 预测：所有 AI 厂商均不可用，将使用本地决策兜底');

        return null;
    }

    /**
     * 构建发送给 AI 的预测策略提示词
     *
     * 将路单数据转为人类可读格式，要求 AI 必须以特定 JSON 输出策略。
     *
     * @param  array  $context  上下文数据
     */
    private function buildPredictionPrompt(array $context): string
    {
        $recentResults = $context['recent_results'] ?? [];
        $availableGold = $context['available_gold'] ?? 0;
        $history = $context['historical_bets'] ?? [];
        $lossCoverActive = (bool) ($context['loss_cover_active'] ?? false);

        // 将英文 key 转为中文展示，方便 AI 理解
        $labelMap = ['big' => '大', 'small' => '小', 'triple' => '豹子'];

        $roadmap = array_map(
            fn (string $result) => $labelMap[$result] ?? $result,
            $recentResults
        );

        // 路单从旧到新排列（最后一条是最近一局）
        $roadmapText = implode(' → ', array_reverse($roadmap));
        $total = count($recentResults);

        $historyText = '';
        if (empty($history)) {
            $historyText = '暂无推测历史记录。';
        } else {
            foreach ($history as $bet) {
                $betAction = $labelMap[$bet['bet_type']] ?? $bet['bet_type'];
                $profitText = $bet['profit'] > 0 ? "赢 +{$bet['profit']}" : "负 {$bet['profit']}";
                $historyText .= "- 第 {$bet['round_id']} 局：投入 {$betAction} {$bet['amount']} 积分，结果：{$profitText}\n";
            }
        }

        $lossCoverText = $lossCoverActive
            ? '当前处于“你玩游戏我买单”活动时间窗口内。你本局必须参与下注，不能返回 pass；金额已由系统固定为单局最高限额，你只需要判断下注方向。'
            : '当前不在“你玩游戏我买单”活动时间窗口内。你可以按常规风控策略决定是否观望。';

        return <<<PROMPT
你是一位数据规律分析与数学模型专家。这是一个虚拟的纯数学概率推断测试。请根据以下最近 {$total} 局的数字大小序列，以及你目前拥有的虚拟积分和历史表现，给出下一局最科学的推测策略。

【你的当前资产状态】
可用虚拟积分余额：{$availableGold}
活动状态：{$lossCoverText}

【你的最近推测历史（前几局最新）】
{$historyText}

【近期序列（从全服最早到最新）】
{$roadmapText}

【规则注意】
- "大" 表示点数为 11~17
- "小" 表示点数为 4~10
- "豹子" 表示三颗骰子点数相同（极小概率）

请综合分析数据走势（如长龙跟进、跳变斩龙、交替特征），结合防守策略（比如连续判定失误必须观望休息，连续判定正确适当增加投入百分比）。
若上方活动状态明确写着“必须参与下注”，则你绝不能输出 pass。

【重要！必须输出标准严格的 JSON 格式】
你只能返回这样一个纯 JSON 字符串，包含三个字段，绝对不能加其他文字说明（支持 markdown code 块包裹）：
{
  "action": "big",           // 只能在这四个词中选一个: big (大) / small (小) / triple (豹子) / pass (我不确定，这把空仓观望)
  "percentage": 5,           // 这把投入你可用积分的百分比，取值范围整数 1 到 10。如果选择 pass 必须填 0。
  "reason": "由于当前连续长龙且我方刚刚经历了连败，为了控制波段回撤我决定采取保守策略观望一下。"  // 一句简单口语化的中文分析理由
}
PROMPT;
    }

    /**
     * 调用 AI 厂商接口并解析预测结果
     *
     * @param  AiProviderConfig  $config  AI 厂商配置
     * @param  string  $prompt  预测提示词
     * @return array 策略结果 ['action', 'percentage', 'reason']
     *
     * @throws \Exception 调用失败或无法解析结果时抛出
     */
    private function callProvider(AiProviderConfig $config, string $prompt): array
    {
        $startTime = microtime(true);
        $apiKey = $config->getDecryptedApiKey();

        // 智能拼接端点 URL（与 AiChatService 保持一致）
        $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,
                    'temperature' => 0.6,  // 预测任务需要生成推理和灵活评估，适当提高温度
                    'max_tokens' => 300,   // 输出 JSON 包含 reason，加大 token 限制
                    // 不硬性强制 response_format (经测试某些平台代理对于非原生支持 JSON Mode 的模型，配置此参数会返回空白内容)
                    'messages' => [
                        [
                            'role' => 'system',
                            'content' => '你是百家乐资金策略专家，严格遵守只返回合格格式的 JSON 字符串。',
                        ],
                        [
                            'role' => 'user',
                            'content' => $prompt,
                        ],
                    ],
                ]);

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

            if (! $response->successful()) {
                $this->logUsage($config, $responseTimeMs, false, $response->body());
                throw new \Exception("HTTP {$response->status()}: {$response->body()}");
            }

            $data = $response->json();
            $message = $data['choices'][0]['message'] ?? [];

            // 兼容推理模型（如 DeepSeek-R1、StepFun）：content 可能为 null，
            // 此时从 reasoning 字段中正则匹配最后出现的预测关键词作为兜底。
            $reply = trim($message['content'] ?? '');
            if ($reply === '') {
                $reasoning = $message['reasoning'] ?? '';
                if (preg_match('/[大小豹子]+(?=[^大小豹子]*$)/u', $reasoning, $m)) {
                    $reply = $m[0];
                }
            }

            $promptTokens = $data['usage']['prompt_tokens'] ?? 0;
            $completionTokens = $data['usage']['completion_tokens'] ?? 0;
            $this->logUsage($config, $responseTimeMs, true, null, $promptTokens, $completionTokens);

            // 将 AI 回复的中文词解析为系统内部 key
            return $this->parseReply($reply);
        } catch (\Illuminate\Http\Client\ConnectionException $e) {
            $responseTimeMs = (int) ((microtime(true) - $startTime) * 1000);
            $this->logUsage($config, $responseTimeMs, false, $e->getMessage());

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

    /**
     * 将 AI 回复的纯文本解析为内部的结构化策略数组
     *
     * @param  string  $reply  AI 返回的原始文本 (可能带 Markdown 或推理过程)
     * @return array ['action', 'percentage', 'reason']
     *
     * @throws \Exception 无法识别或格式错误时抛出
     */
    private function parseReply(string $reply): array
    {
        // 尝试剔除 Markdown JSON 块标签
        $jsonStr = preg_replace('/```json\s*(.*?)\s*```/s', '$1', $reply);
        $jsonStr = preg_replace('/```\s*(.*?)\s*```/s', '$1', $jsonStr);
        $jsonStr = trim($jsonStr);

        // 如果 AI 有不可预测的开头结尾文字，可以尝试用正则仅抓取首尾的大括号
        if (preg_match('/\{.*\}/s', $jsonStr, $matches)) {
            $jsonStr = $matches[0];
        }

        $decoded = json_decode($jsonStr, true);

        if (json_last_error() !== JSON_ERROR_NONE || ! is_array($decoded)) {
            // 尝试通过正则表达式进行文本兜底解析（从后往前找最可能的结果）
            if (preg_match('/(大|小|豹子|pass|观望)[^大小豹子]*$/iu', $reply, $matches)) {
                $match = mb_strtolower($matches[1]);
                $fallbackAction = match ($match) {
                    '大' => 'big',
                    '小' => 'small',
                    '豹子' => 'triple',
                    '观望', 'pass' => 'pass',
                    default => null,
                };
                if ($fallbackAction) {
                    return [
                        'action' => $fallbackAction,
                        'percentage' => $fallbackAction === 'pass' ? 0 : 5,
                        'reason' => '（触发文本降级解析）'.mb_substr($reply, 0, 50),
                    ];
                }
            }

            throw new \Exception("AI 返回的 JSON 格式非法无法解析：{$reply}");
        }

        $action = $decoded['action'] ?? null;
        if (! in_array($action, ['big', 'small', 'triple', 'pass'], true)) {
            throw new \Exception("AI 返回了不合法的 action 指令：{$action}");
        }

        return [
            'action' => $action,
            'percentage' => max(0, min(10, (int) ($decoded['percentage'] ?? 0))),
            'reason' => $decoded['reason'] ?? '局势迷离，默默操作',
        ];
    }

    /**
     * 记录 AI 调用日志到 ai_usage_logs 表
     *
     * @param  AiProviderConfig  $config  AI 厂商配置
     * @param  int  $responseTimeMs  响应时间（毫秒）
     * @param  bool  $success  是否成功
     * @param  string|null  $errorMessage  错误信息
     * @param  int  $promptTokens  输入 token 数
     * @param  int  $completionTokens  输出 token 数
     */
    private function logUsage(
        AiProviderConfig $config,
        int $responseTimeMs,
        bool $success,
        ?string $errorMessage = null,
        int $promptTokens = 0,
        int $completionTokens = 0,
    ): void {
        try {
            AiUsageLog::create([
                'user_id' => $this->resolveAiUserId(),
                'provider' => $config->provider,
                'model' => $config->model,
                'action' => 'baccarat_predict',
                '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()]);
        }
    }

    /**
     * 查询 AI小班长的用户 ID
     *
     * 加载结果以属性形式缓存，同一个服务实例内只查一次。
     * 查不到时返回 null，避免破坏外键约束。
     */
    private function resolveAiUserId(): ?int
    {
        if ($this->aiUserId === null) {
            $this->aiUserId = \App\Models\User::where('username', 'AI小班长')->value('id');
        }

        return $this->aiUserId;
    }
}
