<?php

namespace HaoZiTeam\AIPost\Service\Features;

defined('ABSPATH') || exit;

class WenXinClient {
    private $apiKey;
    private $secretKey;
    private $model;
    private $baseUrl = 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/';
    private $defaultOptions = [];
    // 最大重试次数与退避基准（秒）
    private $maxRetries = 2;
    private $backoffBase = 1.0;
    // 对话消息缓存，兼容 Base.php 中的 addMessage/ask 用法
    private $messages = [];
    // 当前鉴权模式（内部使用），固定为 oauth（按产品要求不再暴露 signature）
    private $authMode = 'oauth';
    
    public function __construct($apiKey, $secretKey, $model = 'ERNIE-Bot-4', array $defaultOptions = []) {
        $this->apiKey = $apiKey;
        $this->secretKey = $secretKey;
        $this->model = $this->normalizeModelName($model);
        $this->defaultOptions = is_array($defaultOptions) ? $defaultOptions : [];
    }

    /**
     * 返回当前使用的模型名
     */
    public function getModel(): string
    {
        return (string)$this->model;
    }

    /**
     * 追加一条消息到会话上下文。
     * @param string $content 消息内容
     * @param string $role 角色：user/assistant/system（system 将被转换为 user 并带标注）
     */
    public function addMessage(string $content, string $role = 'user')
    {
        if (!is_string($content) || $content === '') {
            return $this; // 忽略空消息，保持链式兼容
        }
        $role = strtolower($role);
        if ($role !== 'assistant' && $role !== 'system') {
            $role = 'user';
        }
        // 与 formatMessages 的处理保持一致（system 转 user 并标注）
        if ($role === 'system') {
            $this->messages[] = [
                'role' => 'user',
                'content' => '[System Instruction]\n' . $content,
            ];
        } else {
            $this->messages[] = [
                'role' => $role,
                'content' => $content,
            ];
        }
        return $this;
    }

    /**
     * 清空消息上下文（可选工具方法）。
     */
    public function clearMessages()
    {
        $this->messages = [];
        return $this;
    }

    /**
     * 兼容 Base.php 的 ask 用法：
     * - 将内部缓存的 messages 与本次 prompt 合并，调用 chat()
     * - 返回包含 answer 与 usage 的简化结果
     */
    public function ask(string $prompt, array $options = [])
    {
        $messages = $this->messages;
        $messages[] = [ 'role' => 'user', 'content' => $prompt ];
        $resp = $this->chat($messages, $options);

        // 规范化输出：提取 answer 与 usage，保持与 Base.php 兼容
        $answer = '';
        $usage = [];
        if (is_array($resp)) {
            if (isset($resp['choices'][0]['message']['content']) && is_string($resp['choices'][0]['message']['content'])) {
                $answer = (string)$resp['choices'][0]['message']['content'];
            } elseif (isset($resp['result']) && is_string($resp['result'])) {
                $answer = (string)$resp['result'];
            }
            if (isset($resp['usage']) && is_array($resp['usage'])) {
                $usage = $resp['usage'];
            }
        }
        return [
            'answer' => $answer,
            'usage' => $usage,
            'model' => $this->model,
            'raw' => $resp,
        ];
    }
    
    /**
     * 获取访问令牌
     */
    private function getAccessToken() {
        $cacheKey = 'wenxin_access_token_' . md5($this->apiKey);
        $token = get_transient($cacheKey);
        
        if (false === $token) {
            $url = "https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id={$this->apiKey}&client_secret={$this->secretKey}";
            $response = wp_remote_get($url, [
                // 依赖全局 CA（在 Plugin.php 已设置），保持默认开启验证
                // 'sslverify' => true,
                'timeout' => 30,
            ]);
            
            if (!is_wp_error($response)) {
                $body = json_decode(wp_remote_retrieve_body($response), true);
                $token = $body['access_token'] ?? '';
                if ($token) {
                    // 文心一言access_token有效期为30天，我们设置为29天刷新
                    set_transient($cacheKey, $token, 3600 * 24 * 29);
                }
            }
        }
        
        return $token;
    }
    
    /**
     * 发送聊天请求
     */
    public function chat($messages, $options = []) {
        // 合并默认选项与本次调用选项（调用级覆盖默认级）
        if (is_array($this->defaultOptions) && !empty($this->defaultOptions)) {
            $options = array_merge($this->defaultOptions, is_array($options) ? $options : []);
        }
        // 强制使用 OAuth（不再支持 signature 供用户选择）
        $authMode = 'oauth';
        $this->authMode = $authMode;

        // OAuth + 非流式：直接调用 v2 统一端点，避免 v1 的 Unsupported openapi method
        if ($authMode === 'oauth' && empty($options['stream'])) {
            if (function_exists('error_log')) {
                error_log('AI Post WenXinClient: OAuth 模式优先走 v2/chat/completions');
            }
            return $this->callV2ChatCompletions($messages, $options);
        }
        $path = $this->buildChatPath();
        if (function_exists('error_log')) {
            error_log(sprintf('AI Post WenXinClient: model=%s path=%s auth=%s', (string)$this->model, $path, isset($options['auth_mode']) ? $options['auth_mode'] : 'oauth'));
        }
        if ($authMode === 'oauth') {
            $accessToken = $this->getAccessToken();
            if (empty($accessToken)) {
                throw new \Exception('无法获取文心一言访问令牌，请检查API Key和Secret Key');
            }
            $url = $this->baseUrl . $path . '?access_token=' . $accessToken;
        } else {
            // 直签鉴权模式：不带 access_token，使用 Authorization 签名头
            $url = $this->baseUrl . $path;
        }
        
        // 准备请求数据
        // 扩展参数透传，遵循千帆文档常见字段（若不支持将被忽略）
        $data = wp_parse_args($options, [
            'messages' => $this->formatMessages($messages),
            'stream' => false,               // 目前不启用流式传输（WP HTTP 不易直接消费流）
            'temperature' => 0.7,
            'top_p' => 0.8,
            'penalty_score' => 1.0,
            'max_output_tokens' => 1024,
            // 可选字段（若模型或版本不支持由服务端忽略）
            'stop' => null,
            'user_id' => null,
            'disable_search' => null,
            'enable_citation' => null,
        ]);
        // 按模型限制最大输出上限
        if (!isset($data['max_output_tokens']) || !is_numeric($data['max_output_tokens']) || (int)$data['max_output_tokens'] <= 0) {
            $data['max_output_tokens'] = \HaoZiTeam\AIPost\Service\Features\WenXinModelCatalog::maxOutputFor((string)$this->model);
        } else {
            $catalogMax = \HaoZiTeam\AIPost\Service\Features\WenXinModelCatalog::maxOutputFor((string)$this->model);
            $data['max_output_tokens'] = min((int)$data['max_output_tokens'], $catalogMax);
        }

        // 移除为 null 的可选字段，避免发送多余键
        foreach (['stop','user_id','disable_search','enable_citation'] as $k) {
            if (!array_key_exists($k, $data) || $data[$k] === null) {
                unset($data[$k]);
            }
        }

        // 规范化 stop 字段：允许字符串写法，统一转为数组
        if (isset($data['stop'])) {
            if (is_string($data['stop']) && $data['stop'] !== '') {
                $data['stop'] = [$data['stop']];
            }
            if (!is_array($data['stop'])) {
                unset($data['stop']); // 非法类型不下发
            }
        }

        // 参数安全校验与规范化
        $data = $this->sanitizeOptions($data);
        // 支持流式：当传入 stream=true，将按 SSE 文本解析聚合为完整结果（WP HTTP 会缓冲响应体）
        
        // 允许外部指定超时，默认 60 秒，避免触发 PHP 默认 100s 执行上限
        $timeout = isset($options['timeout']) ? max(1, (int)$options['timeout']) : 60;
        
        // 发送请求（带简单重试与错误码处理）
        $attempt = 0;
        $lastException = null;
        while ($attempt <= $this->maxRetries) {
            $attempt++;
            $headers = [
                'Content-Type' => 'application/json',
                'Accept' => 'application/json',
            ];
            $bodyStr = json_encode($data, JSON_UNESCAPED_UNICODE);
            if ($authMode === 'signature') {
                // 计算 BCE v1 签名头
                $sigHeaders = $this->buildBceSignatureHeaders('POST', $url, $headers, $bodyStr);
                $headers = array_merge($headers, $sigHeaders);
            }
            // 按尝试次数提升本次请求超时：第1次用 $timeout，第2次起翻倍但不超过180s
            $attemptTimeout = (int)min(180, ($attempt === 1 ? $timeout : $timeout * pow(2, $attempt - 1)));
            $response = wp_remote_post($url, [
                'headers' => $headers,
                'body' => $bodyStr,
                'timeout' => $attemptTimeout,
            ]);

            if (is_wp_error($response)) {
                $lastException = new \Exception('文心一言API请求失败: ' . $response->get_error_message());
            } else {
                $statusCode = wp_remote_retrieve_response_code($response);
                $bodyRaw = wp_remote_retrieve_body($response);
                $body = json_decode($bodyRaw, true);

                // 非 200 直接按错误处理
                if ($statusCode !== 200) {
                    $err = $this->buildErrorMessage($statusCode, $body);
                    $lastException = new \Exception($err);
                    // 基于 HTTP 状态码的重试判断
                    if (!$this->shouldRetry(null, $statusCode)) {
                        break;
                    }
                } else {
                    // 优先处理流式：部分场景返回为 SSE 文本（即使 return_raw=true 也会先解析以生成 normalized）
                    if (!is_array($body) && is_string($bodyRaw) && strpos($bodyRaw, 'data:') !== false) {
                        $parsed = $this->parseStream($bodyRaw);
                        if ($parsed !== null) {
                            $normalized = $this->formatResponse($parsed);
                            if (!empty($options['return_raw'])) {
                                return [ 'normalized' => $normalized, 'raw' => $bodyRaw ];
                            }
                            return $normalized;
                        }
                        // 解析失败则继续后续 JSON 逻辑
                        $tmp = json_decode($bodyRaw, true);
                        if (is_array($tmp)) { $body = $tmp; }
                    }
                    // 若仍非数组，尝试普通 JSON 解码（非流式）
                    if (!is_array($body) && is_string($bodyRaw)) {
                        $tmp = json_decode($bodyRaw, true);
                        if (is_array($tmp)) { $body = $tmp; }
                    }
                    // 千帆常见错误体包含 error_code / error_msg
                    if (is_array($body) && isset($body['error_code'])) {
                        // 特判：v1 返回不支持的方法（常见为 error_code=3），回退至 v2 统一端点重试一次
                        if ((int)$body['error_code'] === 3) {
                            if (function_exists('error_log')) {
                                error_log('AI Post WenXinClient: v1 返回 Unsupported openapi method，回退调用 v2/chat/completions');
                            }
                            try {
                                $normalized = $this->callV2ChatCompletions($messages, $options);
                                if (!empty($options['return_raw'])) {
                                    return [ 'normalized' => $normalized, 'raw' => $body ];
                                }
                                return $normalized;
                            } catch (\Exception $e) {
                                $lastException = $e; // 若 v2 也失败，继续走下方重试/失败逻辑
                            }
                        }
                        $shouldRetry = $this->shouldRetry($body['error_code'], 200);
                        $err = $this->buildErrorMessage($statusCode, $body);
                        $lastException = new \Exception($err);
                        if (!$shouldRetry) {
                            break; // 不重试的错误
                        }
                    } else {
                        // 成功
                        $normalized = is_array($body) ? $this->formatResponse($body) : $this->formatResponse(['result' => (string)$body]);
                        if (!empty($options['return_raw'])) {
                            return [ 'normalized' => $normalized, 'raw' => (is_string($bodyRaw) ? $bodyRaw : $body) ];
                        }
                        return $normalized;
                    }
                }
            }

            // 需要重试的情况：指数退避
            if ($attempt <= $this->maxRetries) {
                $sleep = (int)ceil($this->backoffBase * pow(2, $attempt - 1));
                if (function_exists('error_log')) {
                    error_log(sprintf('AI Post WenXinClient: 第 %d 次调用失败，%d 秒后重试（最多 %d 次）。', $attempt, $sleep, $this->maxRetries + 1));
                }
                if ($sleep > 0) { sleep($sleep); }
            }
        }

        // 用最后一次异常信息失败
        throw $lastException ?: new \Exception('文心一言API调用失败（未知原因）');
    }

    /**
     * 根据当前模型构建聊天 Path：chat/{model} 或 chat/completions
     */
    private function buildChatPath(): string
    {
        $model = trim((string)$this->model);
        if ($model !== '' && stripos($model, 'completions') === false) {
            return 'chat/' . rawurlencode($model);
        }
        return 'chat/completions';
    }

    /**
     * 当 v1 接口返回 "Unsupported openapi method"（常见为 error_code=3）时，
     * 回退到 v2 统一端点：POST https://qianfan.baidubce.com/v2/chat/completions
     * 使用直签鉴权（BCE v1），请求体需包含 model。
     */
    private function callV2ChatCompletions(array $messages, array $options = [])
    {
        // v2 端点与 headers（支持 oauth 或 直签）
        $baseUrl = 'https://qianfan.baidubce.com/v2/chat/completions';
        $headers = [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ];

        // 构造请求体：与 v2 对齐，包含 model 与 messages
        $modelV2 = $this->mapModelToV2((string)$this->model);
        $data = wp_parse_args($options, [
            'model' => $modelV2,
            'messages' => $this->formatMessages($messages),
            'stream' => false,
            'temperature' => 0.7,
            'top_p' => 0.8,
            'penalty_score' => 1.0,
            'max_output_tokens' => 1024,
        ]);
        // 按模型限制最大输出上限（v2）
        if (!isset($data['max_output_tokens']) || !is_numeric($data['max_output_tokens']) || (int)$data['max_output_tokens'] <= 0) {
            $data['max_output_tokens'] = \HaoZiTeam\AIPost\Service\Features\WenXinModelCatalog::maxOutputFor((string)$this->model);
        } else {
            $catalogMax = \HaoZiTeam\AIPost\Service\Features\WenXinModelCatalog::maxOutputFor((string)$this->model);
            $data['max_output_tokens'] = min((int)$data['max_output_tokens'], $catalogMax);
        }
        foreach (['stop','user_id','disable_search','enable_citation'] as $k) {
            if (!array_key_exists($k, $data) || $data[$k] === null) {
                unset($data[$k]);
            }
        }
        if (isset($data['stop'])) {
            if (is_string($data['stop']) && $data['stop'] !== '') {
                $data['stop'] = [$data['stop']];
            }
            if (!is_array($data['stop'])) {
                unset($data['stop']);
            }
        }
        $data = $this->sanitizeOptions($data);

        $bodyStr = json_encode($data, JSON_UNESCAPED_UNICODE);
        $url = $baseUrl;
        if ($this->authMode === 'oauth') {
            // OAuth 模式：使用 AIP v2 统一端点（access_token via query）
            $token = $this->getAccessToken();
            if (!$token) {
                throw new \Exception('文心一言API(v2) OAuth 鉴权失败：access_token 不可用');
            }
            $url = 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/completions?access_token=' . rawurlencode($token);
            if (function_exists('error_log')) {
                error_log('AI Post WenXinClient: v2 回退使用 AIP OAuth 端点');
            }
        } else {
            // 当前版本按产品要求不再走直签，保留代码分支以兼容内部调用但不启用
            $sigHeaders = $this->buildBceSignatureHeaders('POST', $url, $headers, $bodyStr);
            $headers = array_merge($headers, $sigHeaders);
            if (function_exists('error_log')) {
                error_log('AI Post WenXinClient: v2 回退使用 Qianfan 直签端点');
            }
        }

        // v2 同样下调默认超时为 60 秒，并引入重试与指数退避
        $timeout = isset($options['timeout']) ? max(1, (int)$options['timeout']) : 60;
        $attempt = 0;
        $lastException = null;
        while ($attempt <= $this->maxRetries) {
            $attempt++;
            $attemptTimeout = (int)min(180, ($attempt === 1 ? $timeout : $timeout * pow(2, $attempt - 1)));
            $response = wp_remote_post($url, [
                'headers' => $headers,
                'body' => $bodyStr,
                'timeout' => $attemptTimeout,
            ]);
            if (is_wp_error($response)) {
                $lastException = new \Exception('文心一言API(v2)请求失败: ' . $response->get_error_message());
            } else {
                $statusCode = wp_remote_retrieve_response_code($response);
                $bodyRaw = wp_remote_retrieve_body($response);
                $body = json_decode($bodyRaw, true);
                if ($statusCode !== 200) {
                    if (function_exists('error_log')) {
                        error_log('AI Post WenXinClient(v2): 非200响应 code=' . $statusCode . ' body=' . (is_string($bodyRaw) ? $bodyRaw : json_encode($body, JSON_UNESCAPED_UNICODE)));
                    }
                    $err = $this->buildErrorMessage($statusCode, $body);
                    $lastException = new \Exception($err);
                    if (!$this->shouldRetry(null, $statusCode)) {
                        break; // 不可重试的状态码，直接失败
                    }
                } else {
                    if (is_array($body) && isset($body['error_code'])) {
                        if (function_exists('error_log')) {
                            error_log('AI Post WenXinClient(v2): 返回错误 error_code=' . $body['error_code'] . ' error_msg=' . ($body['error_msg'] ?? ''));
                        }
                        $err = $this->buildErrorMessage($statusCode, $body);
                        $lastException = new \Exception($err);
                        if (!$this->shouldRetry($body['error_code'], 200)) {
                            break; // 不可重试的业务错误
                        }
                    } else {
                        // 成功
                        return $this->formatResponse($body);
                    }
                }
            }
            if ($attempt <= $this->maxRetries) {
                $sleep = (int)ceil($this->backoffBase * pow(2, $attempt - 1));
                if (function_exists('error_log')) {
                    error_log(sprintf('AI Post WenXinClient(v2): 第 %d 次调用失败，%d 秒后重试（最多 %d 次）。', $attempt, $sleep, $this->maxRetries + 1));
                }
                if ($sleep > 0) { sleep($sleep); }
            }
        }
        throw $lastException ?: new \Exception('文心一言API(v2)调用失败（未知原因）');
    }

    
    /**
     * 规范化模型名（用于内部保留），不强制改写为 v2 ID
     */
    private function normalizeModelName(string $model): string
    {
        return trim((string)$model);
    }

    /**
     * 将上层消息格式化为千帆 messages
     */
    private function formatMessages(array $messages): array
    {
        $out = [];
        foreach ($messages as $msg) {
            if (!is_array($msg)) { continue; }
            $role = strtolower((string)($msg['role'] ?? 'user'));
            $content = (string)($msg['content'] ?? '');
            if ($content === '') { continue; }
            if ($role === 'system') {
                $out[] = [ 'role' => 'user', 'content' => '[System Instruction]\n' . $content ];
            } elseif ($role === 'assistant' || $role === 'user') {
                $out[] = [ 'role' => $role, 'content' => $content ];
            } else {
                $out[] = [ 'role' => 'user', 'content' => $content ];
            }
        }
        if (empty($out)) {
            $out[] = [ 'role' => 'user', 'content' => '' ];
        }
        return $out;
    }

    

    

    /**
     * 是否需要重试（根据 HTTP 状态或千帆错误码）
     */
    private function shouldRetry($errorCode, int $httpStatus): bool
    {
        if (in_array($httpStatus, [429, 500, 502, 503, 504], true)) return true;
        $transientCodes = [18, 336000, 336501, 336503, 336504, 336505];
        if ($errorCode !== null) {
            $code = (int)$errorCode;
            return in_array($code, $transientCodes, true);
        }
        return false;
    }

    /**
     * 构造错误消息
     */
    private function buildErrorMessage(int $httpStatus, $body): string
    {
        $msg = 'HTTP ' . $httpStatus;
        if (is_array($body)) {
            if (isset($body['error_code']) || isset($body['error_msg'])) {
                $msg .= ' | error_code=' . ($body['error_code'] ?? 'n/a') . ' error_msg=' . ($body['error_msg'] ?? '');
            } elseif (isset($body['message'])) {
                $msg .= ' | message=' . $body['message'];
            }
        } elseif (is_string($body) && $body !== '') {
            $msg .= ' | body=' . $body;
        }
        return $msg;
    }

    /**
     * 格式化API响应为统一结构
     */
    private function formatResponse(array $response): array
    {
        // 标准化响应格式，兼容不同的API返回结构
        if (isset($response['choices'][0]['message']['content'])) {
            // OpenAI格式
            return [
                'result' => $response['choices'][0]['message']['content'],
                'usage' => $response['usage'] ?? [],
                'choices' => $response['choices'] ?? [],
                'raw' => $response
            ];
        } elseif (isset($response['result'])) {
            // 百度文心一言格式
            return [
                'result' => $response['result'],
                'usage' => $response['usage'] ?? [],
                'raw' => $response
            ];
        }
        
        return [
            'result' => '',
            'usage' => [],
            'raw' => $response
        ];
    }

    /**
     * 解析流式响应（SSE格式）
     */
    private function parseStream(string $streamData): ?array
    {
        $lines = explode("\n", $streamData);
        $result = '';
        $usage = [];
        
        foreach ($lines as $line) {
            $line = trim($line);
            if (strpos($line, 'data: ') === 0) {
                $data = substr($line, 6);
                if ($data === '[DONE]') {
                    break;
                }
                
                $json = json_decode($data, true);
                if (is_array($json)) {
                    if (isset($json['choices'][0]['delta']['content'])) {
                        $result .= $json['choices'][0]['delta']['content'];
                    } elseif (isset($json['result'])) {
                        $result .= $json['result'];
                    }
                    
                    if (isset($json['usage'])) {
                        $usage = $json['usage'];
                    }
                }
            }
        }
        
        if ($result !== '') {
            return [
                'result' => $result,
                'usage' => $usage
            ];
        }
        
        return null;
    }

    /**
     * 参数安全校验与规范化
     */
    private function sanitizeOptions(array $options): array
    {
        // 温度参数校验
        if (isset($options['temperature'])) {
            $options['temperature'] = max(0.01, min(1.0, (float)$options['temperature']));
        }
        
        // top_p参数校验
        if (isset($options['top_p'])) {
            $options['top_p'] = max(0.01, min(1.0, (float)$options['top_p']));
        }
        
        // penalty_score参数校验
        if (isset($options['penalty_score'])) {
            $options['penalty_score'] = max(1.0, min(2.0, (float)$options['penalty_score']));
        }
        
        // max_output_tokens参数校验（根据日志与官方范围：2-2048）
        if (isset($options['max_output_tokens'])) {
            $options['max_output_tokens'] = max(2, min(2048, (int)$options['max_output_tokens']));
        }
        
        return $options;
    }

    /**
     * 将模型名映射为v2端点支持的格式
     */
    private function mapModelToV2(string $model): string
    {
        $modelMap = [
            'ERNIE-Bot-4' => 'ERNIE-Bot-4',
            'ERNIE-Bot-turbo' => 'ERNIE-Bot-turbo',
            'ERNIE-Bot' => 'ERNIE-Bot',
            'ERNIE-Speed-8K' => 'ERNIE-Speed-8K',
            'ERNIE-Speed-128K' => 'ERNIE-Speed-128K',
            'ERNIE-Lite-8K' => 'ERNIE-Lite-8K',
            'ERNIE-Tiny-8K' => 'ERNIE-Tiny-8K'
        ];
        
        return $modelMap[$model] ?? $model;
    }

    
    /**
     * 构建 BCE v1 签名头（Authorization 与 x-bce-date）。
     * 仅在直签鉴权模式下使用。
     */
    private function buildBceSignatureHeaders(string $method, string $url, array $headers, string $body = ''): array
    {
        $parsed = wp_parse_url($url);
        $host = $parsed['host'] ?? '';
        $path = $parsed['path'] ?? '/';
        $query = isset($parsed['query']) ? $parsed['query'] : '';

        // 基础头：host 与 x-bce-date（ISO8601, UTC）
        $xBceDate = gmdate('Y-m-d\TH:i:s\Z');
        $signedHeadersKeys = ['host','x-bce-date'];
        $canonHeaders = [
            'host' => strtolower($host),
            'x-bce-date' => $xBceDate,
        ];

        // 规范化 URI
        $canonicalURI = $this->bceUriEncodePath($path);

        // 规范化 Query（这里一般为空；若存在则规范化并参与签名）
        $canonicalQuery = $this->bceCanonicalQueryString($query);

        // 规范化 Headers（按 key 排序，使用 key:val\n）
        ksort($canonHeaders);
        $canonicalHeaders = '';
        foreach ($canonHeaders as $k => $v) {
            $canonicalHeaders .= $this->bceUrlEncode($k) . ':' . $this->bceUrlEncode($v) . "\n";
        }

        // 构建 authStringPrefix
        $expiration = 1800; // 30 分钟有效期
        $authStringPrefix = sprintf('bce-auth-v1/%s/%s/%d', $this->apiKey, $xBceDate, $expiration);
        $signingKey = hash_hmac('sha256', $authStringPrefix, $this->secretKey);

        // 构建 CanonicalRequest
        $canonicalRequest = strtoupper($method) . "\n" . $canonicalURI . "\n" . $canonicalQuery . "\n" . $canonicalHeaders;
        $signature = hash_hmac('sha256', $canonicalRequest, $signingKey);

        // 组装 Authorization 头
        sort($signedHeadersKeys);
        $authHeader = $authStringPrefix . '/' . implode(';', $signedHeadersKeys) . '/' . $signature;

        return [
            'Authorization' => $authHeader,
            'x-bce-date' => $xBceDate,
            // host 交由 HTTP 层自动设置，这里无需重复
        ];
    }

    private function bceUriEncodePath(string $path): string
    {
        $segments = explode('/', $path);
        $encoded = array_map(function ($seg) {
            return $this->bceUrlEncode($seg);
        }, $segments);
        // 保留前导斜杠
        if ($path !== '' && $path[0] === '/') {
            return '/' . ltrim(implode('/', $encoded), '/');
        }
        return implode('/', $encoded);
    }

    private function bceCanonicalQueryString(string $query): string
    {
        if ($query === '') return '';
        $pairs = [];
        foreach (explode('&', $query) as $kv) {
            if ($kv === '') continue;
            $parts = explode('=', $kv, 2);
            $k = $this->bceUrlEncode($parts[0]);
            $v = isset($parts[1]) ? $this->bceUrlEncode($parts[1]) : '';
            $pairs[] = $k . '=' . $v;
        }
        sort($pairs, SORT_STRING);
        return implode('&', $pairs);
    }

    private function bceUrlEncode(string $str): string
    {
        // 与 RFC 3986 相容的编码，空格 -> %20，保留 -_.~
        return str_replace('%7E', '~', rawurlencode($str));
    }



    /**
     * 针对常见错误场景提供友好建议（不对具体错误码做绝对断言）。
     */
    private function getErrorHint($errorCode, int $statusCode): string
    {
        $code = (string)$errorCode;
        // 限流/服务繁忙/并发冲突常见场景
        $maybeThrottle = in_array($statusCode, [429, 503], true) || in_array((int)$code, [18, 336000, 336501, 336503, 336504, 336505], true);
        if ($maybeThrottle) {
            return '疑似限流或服务繁忙，建议降低并发与频率、启用多账号轮询、延长重试退避时间。';
        }
        if (in_array($statusCode, [500, 502, 504], true)) {
            return '后端暂时异常，建议稍后重试或增加重试次数。';
        }
        if ($statusCode === 401) {
            return '鉴权失败，请核对 API Key/Secret Key 或 access_token 是否过期。';
        }
        if ($statusCode === 400) {
            return '参数可能不被模型支持，请检查模型名与参数范围（如 temperature/top_p/max_output_tokens）。';
        }
        return '';
    }
}
