package com.emoon.tongue.service.impl;

import cn.hutool.json.JSONUtil;
import com.emoon.tongue.service.ILLMService;
import com.emoon.tongue.utils.HttpUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 大模型服务实现类（机器人端）- 用于字段提炼
 *
 * @author destiny
 * @date 2025-12-21
 */
@Slf4j
@Service("llmRobotService")
public class LLMRobotServiceImpl implements ILLMService {

    @Value("${ai.llm.api-key:}")
    private String llmApiKey;

    @Value("${ai.llm.stream:false}")
    private boolean llmStream;

    @Value("${ai.llm.enable-thinking:false}")
    private boolean llmEnableThinking;

    /**
     * 内网自签名证书场景：跳过 HTTPS 证书/主机名校验（仅建议内网使用）
     */
    @Value("${ai.llm.insecure-ssl:false}")
    private boolean llmInsecureSsl;

    /**
     * 判断主诉是否是中医可以治疗的症状
     *
     * @param chiefComplaintText 主诉原文
     * @param llmUrl 大模型API地址
     * @param llmModel 模型名称
     * @param llmMaxTokens 最大token数
     * @return 模型响应结果（"是"或"否"）
     */
    public String validateChiefComplaint(String chiefComplaintText, String llmUrl, String llmModel, Integer llmMaxTokens) {
        if (chiefComplaintText == null || chiefComplaintText.trim().isEmpty()) {
            throw new IllegalArgumentException("主诉原文不能为空");
        }

        log.info("【判断主诉】LLMRobotService开始调用，主诉原文长度: {}", chiefComplaintText.length());

        // 构建判断主诉的提示词
        String fullPrompt = buildValidationPrompt(chiefComplaintText);

        // 记录发送给模型的完整提示词
        log.info("【判断主诉】发送给模型的提示词: {}", fullPrompt);

        // 调用内部方法（专门用于判断主诉，不提取JSON）
        String response = callLLMInternalForValidation(fullPrompt, llmUrl, llmModel, llmMaxTokens);

        // 记录模型返回的完整结果
        log.info("【判断主诉】模型返回的完整结果: {}", response);

        // 返回提取的"是"或"否"
        return response;
    }

    /**
     * 构建判断主诉的提示词
     */
    private String buildValidationPrompt(String chiefComplaintText) {
        return "请判断以下主诉描述是否是中医可以治疗的症状或病情描述。如果是中医可以治疗的症状，请回答\"是\"；如果不是中医范畴或不明确，请回答\"否\"。只回答\"是\"或\"否\"，不要解释。\n\n" +
                "主诉内容：" + chiefComplaintText;
    }

    /**
     * 内部调用方法（专门用于判断主诉，只提取content，不处理JSON）
     */
    private String callLLMInternalForValidation(String fullPrompt, String llmUrl, String llmModel, Integer llmMaxTokens) {
        if (fullPrompt == null || fullPrompt.trim().isEmpty()) {
            throw new IllegalArgumentException("prompt不能为空");
        }

        log.info("【判断主诉】LLMRobotService开始调用，prompt长度: {}", fullPrompt.length());

        try {
            // 添加随机延迟，避免并发过高（100-300ms）
            Thread.sleep(100 + (int) (Math.random() * 200));

            // 构建消息列表
            List<Map<String, String>> messages = new ArrayList<>();
            messages.add(Map.of("role", "user", "content", fullPrompt));

            // 构建请求体
            Map<String, Object> requestBody = new HashMap<>();
            requestBody.put("model", llmModel);
            requestBody.put("stream", llmStream);
            requestBody.put("temperature", 0.0);
            requestBody.put("messages", messages);
            requestBody.put("max_tokens", llmMaxTokens);
            Map<String, Object> chatTemplateKwargs = new HashMap<>();
            chatTemplateKwargs.put("enable_thinking", llmEnableThinking);
            requestBody.put("chat_template_kwargs", chatTemplateKwargs);

            String jsonBody = JSONUtil.toJsonStr(requestBody);

            // 使用HttpUtils发送POST请求
            log.info("【判断主诉】开始调用大模型API, URL: {}, 请求体长度: {}", llmUrl, jsonBody.length());
            Map<String, String> headers = new HashMap<>();
            if (llmApiKey != null && !llmApiKey.trim().isEmpty()) {
                headers.put("Authorization", "Bearer " + llmApiKey.trim());
            }
            String responseBody = HttpUtils.sendHttpPostByJson(
                llmUrl,
                jsonBody,
                headers.isEmpty() ? null : headers,
                llmInsecureSsl
            );
            log.info("【判断主诉】大模型API调用完成，响应是否为null: {}, 响应内容: {}",
                     responseBody == null, responseBody != null ? responseBody.substring(0, Math.min(200, responseBody.length())) : "null");

            if (responseBody != null && !responseBody.trim().isEmpty()) {
                log.info("【判断主诉】大模型API调用成功，响应长度: {}", responseBody.length());
                // 提取content（"是"或"否"），不处理JSON
                String result = extractValidationContent(responseBody);
                log.info("【判断主诉】提取的判断结果: {}", result);
                return result;
            } else {
                log.error("【判断主诉】API调用失败，响应为空或null。URL: {}, 请求体: {}", llmUrl, jsonBody);
                throw new RuntimeException("API调用失败，响应为空或null");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("调用被中断: " + e.getMessage());
        } catch (Exception e) {
            log.error("【判断主诉】调用大模型API失败，URL: {}, 错误信息: {}", llmUrl, e.getMessage(), e);

            // 检查是否是网络连接问题
            if (e.getMessage() != null && e.getMessage().contains("Connection refused")) {
                throw new RuntimeException("无法连接到大模型API服务，请检查网络连接或API服务状态");
            } else if (e.getMessage() != null && e.getMessage().contains("timeout")) {
                throw new RuntimeException("大模型API调用超时，请稍后重试");
            } else {
                throw new RuntimeException("大模型API调用失败: " + e.getMessage());
            }
        }
    }

    /**
     * 从模型响应中提取判断结果（"是"或"否"）
     */
    private String extractValidationContent(String responseBody) {
        try {
            // 解析大模型API的响应
            Map<String, Object> responseMap = JSONUtil.parseObj(responseBody);

            // 提取 choices[0].message.content
            List<Map<String, Object>> choices = (List<Map<String, Object>>) responseMap.get("choices");
            if (choices != null && !choices.isEmpty()) {
                Map<String, Object> firstChoice = choices.get(0);
                Map<String, Object> message = (Map<String, Object>) firstChoice.get("message");
                if (message != null) {
                    String content = (String) message.get("content");
                    if (content != null && !content.trim().isEmpty()) {
                        return content.trim();
                    }
                }
            }

            // 如果无法提取content，返回原始响应
            log.warn("【判断主诉】无法从响应中提取content，返回原始响应");
            return responseBody;
        } catch (Exception e) {
            log.error("【判断主诉】提取判断结果失败，返回原始响应", e);
            return responseBody;
        }
    }

    /**
     * 调用大模型API进行字段提炼
     * 基于三个问题的回答提炼出结构化字段
     *
     * @param userText 用户对三个问题的回答（合并后的文本）
     * @param llmUrl 大模型API地址
     * @param llmModel 模型名称
     * @param llmMaxTokens 最大token数
     * @return 模型响应结果（JSON字符串）
     */
    public String extractFields(String userText, String llmUrl, String llmModel, Integer llmMaxTokens) {
        if (userText == null || userText.trim().isEmpty()) {
            throw new IllegalArgumentException("用户文本不能为空");
        }

        log.info("LLMRobotService开始调用（字段提炼），用户文本长度: {}", userText.length());

        // 构建完整的提示词
        String fullPrompt = buildExtractionPrompt(userText);

        // 记录发送给模型的完整提示词
        log.info("【字段提炼】发送给模型的提示词: {}", fullPrompt);

        // 调用内部方法
        String response = callLLMInternal(fullPrompt, llmUrl, llmModel, llmMaxTokens);

        // 记录模型返回的完整结果
        log.info("【字段提炼】模型返回的完整结果: {}", response);

        return response;
    }

//    /**
//     * 构建字段提炼的完整提示词
//     */
//    private String buildExtractionPrompt(String userText) {
//        String promptTemplate = "你是中医舌诊信息精准提取工具，用户将一次性提供包含「主诉 + 病程 + 加重 / 缓解因素 + 寒热 + 汗出 + 饮食口感 + 二便 + 睡眠 + 精神情志」的语音转文字内容，你需严格按以下规则提取所有字段，仅输出最终 JSON，无任何额外解释、不篡改语义、不自由发挥：\n\n" +
//            "\n" +
//            "核心规则 1：枚举字段强制约束（仅选唯一匹配项，无匹配则填 \"无\"）\n\n" +
//            "各字段枚举值清单（必须完全匹配，不可自定义）及映射规则：\n\n" +
//            "1. duration\n\n" +
//            "可选值：≤3 天、1 周内、1 月内、3 月内、半年以上、数年反复发作\n\n" +
//            "映射规则：用户描述模糊时自动匹配最近分桶（如 \"半个月\"→1 月内、\"两三个月\"→3 月内、\"大半年\"→半年以上）\n\n" +
//            "2. cold_heat\n\n" +
//            "可选值：怕冷、怕热、寒热交替、都不明显\n\n" +
//            "映射规则：用户表述矛盾（如 \"又怕冷又怕热\"）→\"寒热交替\"；未提寒热倾向→\"都不明显\"\n\n" +
//            "3. sweating\n\n" +
//            "可选值：正常、无汗、自汗、盗汗、半身汗\n\n" +
//            "映射规则：\"一动就出汗\"→\"自汗\"、\"晚上睡觉出汗\"→\"盗汗\"、\"只有一侧出汗\"→\"半身汗\"\n\n" +
//            "4. mouth_feel\n\n" +
//            "可选值：正常、口干喜饮、口干不喜饮、口苦、口淡、口甜、口黏、口中异味\n\n" +
//            "映射规则：仅说 \"口干\" 未提是否喜饮→\"口干喜饮\"；\"嘴里发苦\"→\"口苦\"；\"嘴里黏糊糊\"→\"口黏\"\n\n" +
//            "5. appetite\n\n" +
//            "可选值：正常、食欲不振、多食易饥、饥不欲食\n\n" +
//            "映射规则：\"不想吃饭\"→\"食欲不振\"；\"吃得多还容易饿\"→\"多食易饥\"；\"饿但不想吃\"→\"饥不欲食\"\n\n" +
//            "6. after_meal\n\n" +
//            "可选值：正常、腹胀、腹痛、反酸、烧心、恶心呕吐\n\n" +
//            "映射规则：多症状并存时选最明显的（如 \"又胀又反酸\"→\"反酸\"）；\"吃完胃里烧得慌\"→\"烧心\"\n\n" +
//            "7. stool\n\n" +
//            "可选值：正常、偏干、偏稀、黏滞不爽、便秘与腹泻交替\n\n" +
//            "映射规则：\"大便干结\"→\"偏干\"；\"大便不成形\"→\"偏稀\"；\"大便粘马桶\"→\"黏滞不爽\"\n\n" +
//            "8. urination\n\n" +
//            "可选值：正常、尿黄赤、尿清长、尿频、尿急、夜尿多 1 次、夜尿多 2 次及以上、小便不利\n\n" +
//            "映射规则：仅说 \"夜尿多\"→\"夜尿多 1 次\"；\"小便发黄\"→\"尿黄赤\"；\"小便费劲\"→\"小便不利\"\n\n" +
//            "9. sleep\n\n" +
//            "可选值：正常、入睡困难、睡眠浅易醒、早醒、多梦、嗜睡\n\n" +
//            "映射规则：多症状并存时选最核心的（如 \"难入睡还易醒\"→\"入睡困难\"）；\"总犯困\"→\"嗜睡\"\n\n" +
//            "10. emotion\n\n" +
//            "可选值：平和、烦躁易怒、抑郁低落、焦虑紧张、思虑过度\n\n" +
//            "映射规则：\"容易发火\"→\"烦躁易怒\"；\"心情郁闷\"→\"抑郁低落\"；\"想太多睡不着\"→\"思虑过度\"\n\n" +
//            "11. energy\n\n" +
//            "可选值：精力充沛、神疲乏力、气短懒言\n\n" +
//            "映射规则：\"浑身没力气\"→\"神疲乏力\"；\"说话都没劲儿\"→\"气短懒言\"\n\n" +
//            "核心规则 2：非枚举字段提取约束\n\n" +
//            "1. chief_complaint_text\n\n" +
//            "提取用户原话中最困扰的症状描述，去除语气词、重复内容（如 \"嗯… 我头痛，就是头部胀痛\"→\"我头痛，头部胀痛\"），不可删减关键信息\n\n" +
//            "2. chief_complaints\n\n" +
//            "从用户描述中提取 1-3 个中医常见症状标签（如 \"头痛\"\"胃胀\"\"关节痛\"），不可新增未提及症状，不足 1 个填 \"无\"\n\n" +
//            "3. aggravating_relieving_factors\n\n" +
//            "精简提取用户描述的加重 / 缓解因素（如 \"遇冷加重、休息后缓解\"），无相关信息填 \"无\"\n\n" +
//            "4. sex/age\n\n" +
//            "由系统自动带入，你无需提取，直接保留字段即可\n\n" +
//            "5. need_followup\n\n" +
//            "仅 boolean 类型（true/false），判断规则：核心字段（chief_complaints、duration、cold_heat、stool、sleep）中≥2 个为 \"无\" 则填 true，否则填 false\n\n" +
//            "***输出 JSON 结构（字段顺序不可调整、不可遗漏）***\n\n" +
//            "{\"sex\": \"\",\"age\": \"\",\"chief_complaint_text\": \"无\",\"chief_complaints\": [\"无\"],\"duration\": \"无\",\"aggravating_relieving_factors\": \"无\",\"cold_heat\": \"无\",\"sweating\": \"无\",\"mouth_feel\": \"无\",\"appetite\": \"无\",\"after_meal\": \"无\",\"stool\": \"无\",\"urination\": \"无\",\"sleep\": \"无\",\"emotion\": \"无\",\"energy\": \"无\",\"tongue_features_json\": {},\"need_followup\": false}\n\n" +
//            "执行要求\n\n" +
//            "所有枚举字段值必须与清单完全一致，不可用同义词、缩写（如 \"怕热\" 不可写 \"燥热\"，\"自汗\" 不可写 \"易出汗\"）；\n\n" +
//            "即使用户未提及某类信息，对应字段也需按模板填 \"无\"（boolean 字段除外）；\n\n" +
//            "JSON 语法必须正确（引号闭合、无多余逗号、字段值类型匹配）。\n\n" +
//            "用户语音转文字内容：{user_text}\n\n" +
//            "直接输出符合所有规则的 JSON，无需任何其他内容！";
//
//        return promptTemplate.replace("{user_text}", userText);
//    }

    /**
     * 构建字段提炼的完整提示词
     */
    private String buildExtractionPrompt(String userText) {
        String promptTemplate =
            "你是中医舌诊信息精准提取工具，用户将一次性提供包含「主诉 + 病程 + 加重 / 缓解因素 + 寒热 + 汗出 + 饮食口感 + 二便 + 睡眠 + 精神情志」的语音转文字内容，你需严格按以下规则提取所有字段，仅输出最终 JSON，无任何额外解释、不篡改语义、不自由发挥：\n\n" +

                "核心规则 1：枚举字段强制约束（仅选唯一匹配项，无匹配则填 \"无\"）\n\n" +
                "各字段枚举值清单（必须完全匹配，不可自定义）及映射规则：\n\n" +

                "1. duration\n\n" +
                "可选值：≤3 天、1 周内、1 月内、3 月内、半年以上、数年反复发作\n\n" +
                "映射规则：用户描述模糊时自动匹配最近分桶（如 \"半个月\"→1 月内、\"两三个月\"→3 月内、\"大半年\"→半年以上）\n\n" +

                "2. cold_heat\n\n" +
                "可选值：怕冷、怕热、寒热交替、都不明显\n\n" +
                "映射规则：用户表述矛盾（如 \"又怕冷又怕热\"）→\"寒热交替\"；未提寒热倾向→\"都不明显\"\n\n" +

                "3. sweating\n\n" +
                "可选值：正常、无汗、自汗、盗汗、半身汗\n\n" +
                "映射规则：\"一动就出汗\"→\"自汗\"、\"晚上睡觉出汗\"→\"盗汗\"、\"只有一侧出汗\"→\"半身汗\"\n\n" +

                "4. mouth_feel\n\n" +
                "可选值：正常、口干喜饮、口干不喜饮、口苦、口淡、口甜、口黏、口中异味\n\n" +
                "映射规则：仅说 \"口干\" 未提是否喜饮→\"口干喜饮\"；\"嘴里发苦\"→\"口苦\"；\"嘴里黏糊糊\"→\"口黏\"\n\n" +

                "5. appetite\n\n" +
                "可选值：正常、食欲不振、多食易饥、饥不欲食\n\n" +
                "映射规则：\"不想吃饭\"→\"食欲不振\"；\"吃得多还容易饿\"→\"多食易饥\"；\"饿但不想吃\"→\"饥不欲食\"\n\n" +

                "6. after_meal\n\n" +
                "可选值：正常、腹胀、腹痛、反酸、烧心、恶心呕吐\n\n" +
                "映射规则：多症状并存时选最明显的（如 \"又胀又反酸\"→\"反酸\"）；\"吃完胃里烧得慌\"→\"烧心\"\n\n" +

                "7. stool\n\n" +
                "可选值：正常、偏干、偏稀、黏滞不爽、便秘与腹泻交替\n\n" +
                "映射规则：\"大便干结\"→\"偏干\"；\"大便不成形\"→\"偏稀\"；\"大便粘马桶\"→\"黏滞不爽\"\n\n" +

                "8. urination\n\n" +
                "可选值：正常、尿黄赤、尿清长、尿频、尿急、夜尿多 1 次、夜尿多 2 次及以上、小便不利\n\n" +
                "映射规则：仅说 \"夜尿多\"→\"夜尿多 1 次\"；\"小便发黄\"→\"尿黄赤\"；\"小便费劲\"→\"小便不利\"\n\n" +

                "9. sleep\n\n" +
                "可选值：正常、入睡困难、睡眠浅易醒、早醒、多梦、嗜睡\n\n" +
                "映射规则：多症状并存时选最核心的（如 \"难入睡还易醒\"→\"入睡困难\"）；\"总犯困\"→\"嗜睡\"\n\n" +

                "10. emotion\n\n" +
                "可选值：平和、烦躁易怒、抑郁低落、焦虑紧张、思虑过度\n\n" +
                "映射规则：\"容易发火\"→\"烦躁易怒\"；\"心情郁闷\"→\"抑郁低落\"；\"想太多睡不着\"→\"思虑过度\"\n\n" +

                "11. energy\n\n" +
                "可选值：精力充沛、神疲乏力、气短懒言\n\n" +
                "映射规则：\"浑身没力气\"→\"神疲乏力\"；\"说话都没劲儿\"→\"气短懒言\"\n\n" +

                "核心规则 2：非枚举字段提取约束\n\n" +

                "1. chief_complaint_text\n\n" +
                "提取用户原话中最困扰的症状描述，去除语气词、重复内容，不可删减关键信息\n\n" +

                "2. chief_complaints\n\n" +
                "从用户描述中提取 1-3 个中医常见症状标签，不可新增未提及症状，不足 1 个填 \"无\"\n\n" +

                "3. aggravating_relieving_factors\n\n" +
                "精简提取用户描述的加重 / 缓解因素，无相关信息填 \"无\"\n\n" +

                "4. sex/age\n\n" +
                "由系统自动带入，你无需提取，直接保留字段即可\n\n" +

                "5. need_followup\n\n" +
                "仅 boolean 类型（true/false），判断规则：核心字段（chief_complaints、duration、cold_heat、stool、sleep）中≥2 个为 \"无\" 则填 true，否则填 false\n\n" +

                "⚠️【强制 JSON 完整输出约束（必须严格遵守）】⚠️\n" +
                "你必须在一次连续输出中完整生成一个合法 JSON 对象，不得中途结束。\n" +
                "即使后续字段全部为 \"无\"，也必须逐字段完整输出。\n" +
                "JSON 末尾必须包含 \"__end__\": true，在该字段和最终右大括号输出完成之前，禁止结束输出。\n\n" +

                "***输出 JSON 结构（字段顺序不可调整、不可遗漏）***\n\n" +

                "{"
                + "\"sex\":\"\","
                + "\"age\":\"\","
                + "\"chief_complaint_text\":\"无\","
                + "\"chief_complaints\":[\"无\"],"
                + "\"duration\":\"无\","
                + "\"aggravating_relieving_factors\":\"无\","
                + "\"cold_heat\":\"无\","
                + "\"sweating\":\"无\","
                + "\"mouth_feel\":\"无\","
                + "\"appetite\":\"无\","
                + "\"after_meal\":\"无\","
                + "\"stool\":\"无\","
                + "\"urination\":\"无\","
                + "\"sleep\":\"无\","
                + "\"emotion\":\"无\","
                + "\"energy\":\"无\","
                + "\"tongue_features_json\":{},"
                + "\"need_followup\":false,"
                + "\"__end__\":true"
                + "}\n\n" +

                "用户语音转文字内容：{user_text}\n\n" +
                "直接输出符合所有规则的 JSON，无需任何其他内容！";

        return promptTemplate.replace("{user_text}", userText);
    }

    /**
     * 实现接口方法（兼容原有调用方式）
     */
    @Override
    public String callLLM(String prompt, String llmUrl, String llmModel, Integer llmMaxTokens) {
        log.info("【LLMRobotService】callLLM方法被调用，prompt长度: {}", prompt != null ? prompt.length() : 0);

        // 如果prompt已经是完整的提示词，直接使用；否则作为用户文本构建完整提示词
        if (prompt != null && prompt.contains("你是中医舌诊信息精准提取工具")) {
            // 已经是完整提示词，直接调用
            log.info("【LLMRobotService】检测到完整提示词，直接调用callLLMInternal");
            return callLLMInternal(prompt, llmUrl, llmModel, llmMaxTokens);
        } else {
            // 作为用户文本，构建完整提示词
            log.info("【LLMRobotService】作为用户文本处理，调用extractFields");
            return extractFields(prompt, llmUrl, llmModel, llmMaxTokens);
        }
    }

    /**
     * 内部调用方法
     */
    private String callLLMInternal(String fullPrompt, String llmUrl, String llmModel, Integer llmMaxTokens) {
        if (fullPrompt == null || fullPrompt.trim().isEmpty()) {
            throw new IllegalArgumentException("prompt不能为空");
        }

        log.info("LLMRobotService开始调用（字段提炼），prompt长度: {}", fullPrompt.length());

        try {
            // 添加随机延迟，避免并发过高（100-300ms）
            Thread.sleep(100 + (int) (Math.random() * 200));

            // 构建消息列表
            List<Map<String, String>> messages = new ArrayList<>();
            messages.add(Map.of("role", "user", "content", fullPrompt));

            // 构建请求体
            Map<String, Object> requestBody = new HashMap<>();
            requestBody.put("model", llmModel);
            // 注意：本项目后端按"非流式 JSON"解析，因此默认关闭 stream
            requestBody.put("stream", llmStream);
            requestBody.put("temperature", 0.0);
            requestBody.put("messages", messages);
            requestBody.put("max_tokens", llmMaxTokens);
            Map<String, Object> chatTemplateKwargs = new HashMap<>();
            chatTemplateKwargs.put("enable_thinking", llmEnableThinking);
            requestBody.put("chat_template_kwargs", chatTemplateKwargs);

            String jsonBody = JSONUtil.toJsonStr(requestBody);

            Map<String, String> headers = new HashMap<>();
            if (llmApiKey != null && !llmApiKey.trim().isEmpty()) {
                headers.put("Authorization", "Bearer " + llmApiKey.trim());
            }

            // 至多调用大模型两次：第一次失败则重试一次
            for (int attempt = 1; attempt <= 2; attempt++) {
                log.info("开始调用大模型API（字段提炼），第 {} 次尝试, URL: {}, 请求体长度: {}", attempt, llmUrl, jsonBody.length());
                String responseBody = HttpUtils.sendHttpPostByJson(
                    llmUrl,
                    jsonBody,
                    headers.isEmpty() ? null : headers,
                    llmInsecureSsl
                );
                log.info("大模型API调用完成（字段提炼），第 {} 次尝试，响应是否为null: {}, 完整响应内容为: {}",
                         attempt,
                         responseBody == null,
                         responseBody);

                if (responseBody == null || responseBody.trim().isEmpty()) {
                    log.error("API调用失败，响应为空或null。URL: {}, 请求体: {}", llmUrl, jsonBody);
                    if (attempt == 2) {
                        throw new RuntimeException("API调用失败，响应为空或null");
                    }
                    // 否则继续下一次循环重试
                    continue;
                }

                log.info("大模型API调用成功（字段提炼），第 {} 次尝试，响应长度: {}", attempt, responseBody.length());
                // 提取并清理JSON内容
                String extractedJson = extractAndCleanJson(responseBody);

                // 在后端先尝试解析一次，确保是合法 JSON；如果失败则重试一次
                try {
                    JSONUtil.parseObj(extractedJson);
                    log.info("【字段提炼】第 {} 次尝试解析 JSON 成功", attempt);
                    return extractedJson;
                } catch (Exception parseEx) {
                    log.warn("【字段提炼】第 {} 次尝试解析 JSON 失败，将重试一次。错误信息: {}", attempt, parseEx.getMessage());
                    if (attempt == 2) {
                        // 第二次仍然解析失败，则把最后一次结果返回给前端，由前端做兜底处理
                        log.error("【字段提炼】多次尝试后仍无法解析为标准 JSON，返回最后一次提取结果");
                        return extractedJson;
                    }
                    // 否则继续下一次循环重试
                }
            }

            // 理论上不会走到这里
            throw new RuntimeException("大模型API调用失败：未知错误");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("调用被中断: " + e.getMessage());
        } catch (Exception e) {
            log.error("调用大模型API失败（字段提炼），URL: {}, 错误信息: {}", llmUrl, e.getMessage(), e);

            // 检查是否是网络连接问题
            if (e.getMessage() != null && e.getMessage().contains("Connection refused")) {
                throw new RuntimeException("无法连接到大模型API服务，请检查网络连接或API服务状态");
            } else if (e.getMessage() != null && e.getMessage().contains("timeout")) {
                throw new RuntimeException("大模型API调用超时，请稍后重试");
            } else {
                throw new RuntimeException("大模型API调用失败: " + e.getMessage());
            }
        }
    }

    /**
     * 从大模型响应中提取并清理JSON内容
     */
    private String extractAndCleanJson(String responseBody) {
        try {
            // 解析大模型API的响应
            Map<String, Object> responseMap = JSONUtil.parseObj(responseBody);

            // 提取 choices[0].message.content
            List<Map<String, Object>> choices = (List<Map<String, Object>>) responseMap.get("choices");
            if (choices != null && !choices.isEmpty()) {
                Map<String, Object> firstChoice = choices.get(0);
                Map<String, Object> message = (Map<String, Object>) firstChoice.get("message");
                if (message != null) {
                    String content = (String) message.get("content");
                    if (content != null && !content.trim().isEmpty()) {
                        // 移除markdown代码块标记（如果有）
                        String jsonStr = content.trim();
                        if (jsonStr.startsWith("```")) {
                            jsonStr = jsonStr.replaceAll("```json\\s*", "").replaceAll("```\\s*", "").trim();
                        }

                        // 清理乱码：修复常见的识别错误
                        jsonStr = cleanJsonString(jsonStr);

                        log.info("【字段提炼】提取并清理后的JSON内容: {}", jsonStr);
                        return jsonStr;
                    }
                }
            }

            // 如果无法提取content，返回原始响应
            log.warn("无法从响应中提取content，返回原始响应");
            return responseBody;
        } catch (Exception e) {
            log.error("提取JSON内容失败，返回原始响应", e);
            return responseBody;
        }
    }

    /**
     * 清理JSON字符串中的乱码和语法错误
     */
    private String cleanJsonString(String jsonStr) {
        if (jsonStr == null || jsonStr.isEmpty()) {
            return jsonStr;
        }

        // 修复常见的识别错误
        // "ur游戏副本" -> "urination"
        jsonStr = jsonStr.replace("ur游戏副本", "urination");
        jsonStr = jsonStr.replace("ur游戏", "urination");
        jsonStr = jsonStr.replace("ur副本", "urination");

        // 其他可能的乱码修复
        jsonStr = jsonStr.replace("\\\"ur游戏副本\\\"", "\"urination\"");
        jsonStr = jsonStr.replace("\\\"ur游戏\\\"", "\"urination\"");

        // 修复JSON语法错误：将 "字段名: " 修复为 "字段名": "
        // 例如："urination: " -> "urination": "
        jsonStr = jsonStr.replaceAll("\"([a-z_]+):\\s*\"", "\"$1\": \"");

        return jsonStr;
    }

}

