package com.agent.management.engine;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;

import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Supplier;

/**
 * LLM 结构化输出辅助类。
 *
 * <p>当节点声明 outputs 时（即使只有 1 个），通过 Prompt 注入指令让模型按 JSON 格式输出，
 * 再解析拆分到各变量，并按 {@link RetryPolicy} 自动重试。</p>
 *
 * <p><b>触发条件</b>：{@code data.outputs} 是数组且非空。</p>
 *
 * <p><b>双层重试模型</b>：</p>
 * <ul>
 *   <li><b>外层（运行重试）</b>：LLM 调用异常或空响应时触发，重新执行完整推理。
 *       次数由 {@link RetryPolicy#runtimeMaxRetries} 控制。</li>
 *   <li><b>内层（格式重试）</b>：JSON 解析失败、字段缺失、类型不匹配、键名不匹配时触发，
 *       仅把上次响应 + JSON 模板 + 错误说明回喂给 LLM，让其重新组织 JSON 结构，
 *       <b>不</b>重新执行业务逻辑。次数由 {@link RetryPolicy#formatMaxRetries} 控制。</li>
 * </ul>
 *
 * <p>内层重试耗尽后，根据失败原因判断是否升级到外层重试（重新完整推理）。</p>
 */
@Slf4j
public final class StructuredOutputHelper {

    private static final ObjectMapper MAPPER = new ObjectMapper();

    /** 重组 prompt 中上次响应的截断长度，避免 token 超限。 */
    private static final int REFORMAT_RESPONSE_MAX_LEN = 2000;

    private StructuredOutputHelper() {}

    // ========== 触发条件 ==========

    /**
     * 判断节点是否需要结构化输出。
     * 触发条件：data.outputs 是数组且非空（移除了 v1 的 size > 1 限制）。
     */
    public static boolean needsStructuredOutput(JsonNode data) {
        JsonNode outputs = data.path("outputs");
        return outputs.isArray() && !outputs.isEmpty();
    }

    // ========== 提取结果 ==========

    /**
     * LLM 结构化输出提取结果。
     * - {@code errorMessage == null} 表示成功，{@code data} 为解析后的字段 Map
     * - 否则表示重试耗尽仍失败，{@code data} 为空，{@code attempts} 为累计调用次数
     */
    public static final class ExtractionResult {
        public final Map<String, Object> data;
        public final String errorMessage;
        public final int attempts;

        private ExtractionResult(Map<String, Object> data, String errorMessage, int attempts) {
            this.data = data == null ? Collections.emptyMap() : data;
            this.errorMessage = errorMessage;
            this.attempts = attempts;
        }

        public static ExtractionResult ok(Map<String, Object> data, int attempts) {
            return new ExtractionResult(data, null, attempts);
        }

        public static ExtractionResult fail(String errorMessage, int attempts) {
            return new ExtractionResult(Collections.emptyMap(), errorMessage, attempts);
        }
    }

    // ========== 主入口（双层重试） ==========

    /**
     * 带 retry 的结构化输出提取（双层重试模型）。
     *
     * @param client        ChatClient
     * @param userPrompt    用户提示词（不含结构化指令）
     * @param systemPrompt  系统提示词（可为空）
     * @param outputsDecl   节点 outputs 声明数组
     * @param retryPolicy   重试策略
     * @return ExtractionResult
     */
    public static ExtractionResult extractWithRetry(ChatClient client,
                                                     String userPrompt,
                                                     String systemPrompt,
                                                     JsonNode outputsDecl,
                                                     RetryPolicy retryPolicy) {
        if (client == null) {
            return ExtractionResult.fail("ChatClient 为 null", 0);
        }
        if (outputsDecl == null || !outputsDecl.isArray() || outputsDecl.isEmpty()) {
            return ExtractionResult.fail("outputs 声明为空", 0);
        }

        String lastError = null;
        int totalAttempts = 0;

        // ===== 外层：运行重试（重新完整推理） =====
        for (int runtimeAttempt = 1; runtimeAttempt <= retryPolicy.runtimeMaxRetries; runtimeAttempt++) {

            // 1. 调用 LLM 完整推理（首外层不带错误回喂，后续外层把上次错误加入 instruction）
            String response;
            try {
                response = invokeFullLlm(client, userPrompt, systemPrompt, outputsDecl,
                        runtimeAttempt > 1 ? lastError : null);
            } catch (Exception e) {
                log.warn("[StructuredOutput] LLM 调用异常 runtimeAttempt={}/{}: {}",
                        runtimeAttempt, retryPolicy.runtimeMaxRetries, e.getMessage());
                lastError = "LLM 调用异常: " + e.getMessage();
                if (!retryPolicy.retryOnParseError) break;
                continue;
            }
            totalAttempts++;

            if (response == null || response.isBlank()) {
                lastError = "LLM 返回空结果";
                if (!retryPolicy.retryOnParseError) break;
                continue;
            }

            // 2. 内层：格式重试（仅重组 JSON，不重跑逻辑）
            ReformatOutcome outcome = reformatLoop(client, systemPrompt, response, outputsDecl, retryPolicy);
            totalAttempts += outcome.reformatLlmCalls;

            if (outcome.ok) {
                log.info("[StructuredOutput] 提取成功 runtimeAttempt={}/{}, reformatLlmCalls={}",
                        runtimeAttempt, retryPolicy.runtimeMaxRetries, outcome.reformatLlmCalls);
                return ExtractionResult.ok(outcome.data, totalAttempts);
            }

            // 3. 内层耗尽 → 判断是否升级到外层重试
            lastError = outcome.errorMessage;
            if (!shouldRuntimeRetry(outcome.failReason, retryPolicy)) break;
        }

        return ExtractionResult.fail(lastError == null ? "提取失败" : lastError,
                Math.max(totalAttempts, 1));
    }

    // ========== 从已有文本提取（Hermes / Agent 系列接入入口） ==========

    /**
     * 从已有的原始文本（如 Hermes Agent 的 finalText）出发，做结构化提取与双层重试。
     *
     * <p><b>与 {@link #extractWithRetry} 的差异</b>：</p>
     * <ul>
     *   <li>外层（运行重试）：调用 {@code runtimeRetrySupplier} 重新生成原始文本
     *       （如 Hermes 在同一 session 内重新执行 Agent），而非调用 LLM 完整推理</li>
     *   <li>内层（格式重试）：仍用 {@code reformatClient}（普通 LLM）做 JSON 重组，
     *       与 {@link #extractWithRetry} 内层逻辑完全一致</li>
     * </ul>
     *
     * <p><b>职责分离</b>：</p>
     * <ul>
     *   <li>Hermes：业务推理、工具调用、产出 rawText（昂贵，少跑）</li>
     *   <li>普通 LLM：把 rawText 整理成符合 schema 的 JSON（廉价，可多次）</li>
     * </ul>
     *
     * @param rawText              已有的原始文本（如 Hermes finalText）
     * @param outputsDecl          节点 outputs 声明数组
     * @param retryPolicy          重试策略
     * @param reformatClient       内层重组用的 ChatClient（普通 LLM），null 时跳过内层重组
     * @param runtimeRetrySupplier 外层重试 supplier，返回新的 rawText；null 时不做外层重试
     * @return ExtractionResult
     */
    public static ExtractionResult extractWithRetryFromText(String rawText,
                                                             JsonNode outputsDecl,
                                                             RetryPolicy retryPolicy,
                                                             ChatClient reformatClient,
                                                             Supplier<String> runtimeRetrySupplier) {
        if (outputsDecl == null || !outputsDecl.isArray() || outputsDecl.isEmpty()) {
            return ExtractionResult.fail("outputs 声明为空", 0);
        }
        if (rawText == null || rawText.isBlank()) {
            return ExtractionResult.fail("原始响应为空", 0);
        }
        if (reformatClient == null) {
            return ExtractionResult.fail("reformatClient 为 null", 0);
        }

        String lastError = null;
        int totalAttempts = 0;
        String currentText = rawText;

        // ===== 外层：runtime retry（用 supplier 重新生成原始文本） =====
        for (int runtimeAttempt = 1; runtimeAttempt <= retryPolicy.runtimeMaxRetries; runtimeAttempt++) {

            // 第 2 次起：调用 supplier 重新生成 rawText
            if (runtimeAttempt > 1) {
                if (runtimeRetrySupplier == null) {
                    break;
                }
                try {
                    String newText = runtimeRetrySupplier.get();
                    totalAttempts++;
                    if (newText == null || newText.isBlank()) {
                        lastError = "runtimeRetrySupplier 返回空结果";
                        log.warn("[StructuredOutput] supplier 空结果 runtimeAttempt={}/{}",
                                runtimeAttempt, retryPolicy.runtimeMaxRetries);
                        if (!retryPolicy.retryOnParseError) break;
                        continue;
                    }
                    currentText = newText;
                } catch (Exception e) {
                    log.warn("[StructuredOutput] runtimeRetrySupplier 异常 attempt={}/{}: {}",
                            runtimeAttempt, retryPolicy.runtimeMaxRetries, e.getMessage());
                    lastError = "runtimeRetrySupplier 异常: " + e.getMessage();
                    if (!retryPolicy.retryOnParseError) break;
                    continue;
                }
            }

            // 内层：reformat loop（用 reformatClient 做 JSON 重组）
            ReformatOutcome outcome = reformatLoop(reformatClient, null, currentText, outputsDecl, retryPolicy);
            totalAttempts += outcome.reformatLlmCalls;

            if (outcome.ok) {
                log.info("[StructuredOutput] 从文本提取成功 runtimeAttempt={}/{}, reformatLlmCalls={}",
                        runtimeAttempt, retryPolicy.runtimeMaxRetries, outcome.reformatLlmCalls);
                return ExtractionResult.ok(outcome.data, totalAttempts);
            }

            lastError = outcome.errorMessage;
            if (!shouldRuntimeRetry(outcome.failReason, retryPolicy)) break;
        }

        return ExtractionResult.fail(lastError == null ? "提取失败" : lastError,
                Math.max(totalAttempts, 1));
    }

    // ========== 内层循环：仅重组 JSON ==========

    /**
     * 内层循环：解析 + strict 校验 + 重组重试。
     * 失败时通过重组 prompt 让 LLM 仅重组 JSON 结构，不重新执行业务逻辑。
     */
    private static ReformatOutcome reformatLoop(ChatClient client,
                                                 String systemPrompt,
                                                 String originalResponse,
                                                 JsonNode outputsDecl,
                                                 RetryPolicy retryPolicy) {
        String currentResponse = originalResponse;
        String lastError = null;
        FailReason lastFailReason = null;
        int reformatLlmCalls = 0;

        for (int i = 1; i <= retryPolicy.formatMaxRetries; i++) {

            // 1. 解析
            Map<String, Object> parsed = parse(currentResponse, outputsDecl);
            if (parsed == null) {
                lastError = "JSON 解析失败，原始响应前 200 字: "
                        + currentResponse.substring(0, Math.min(currentResponse.length(), 200));
                lastFailReason = FailReason.PARSE_ERROR;
                if (!retryPolicy.retryOnParseError) break;
            } else {
                // 2. strict 校验（所有声明字段视为必填，键名精确匹配）
                OutputValidator.ValidationResult validation =
                        OutputValidator.validate(parsed, outputsDecl, true);

                if (validation.ok) {
                    return ReformatOutcome.ok(validation.converted, reformatLlmCalls);
                }

                lastError = buildValidationErrorMsg(validation);
                lastFailReason = classifyFailReason(validation);
                boolean shouldRetry =
                        (!validation.missingRequired.isEmpty() && retryPolicy.retryOnMissingRequired)
                                || (!validation.typeMismatched.isEmpty() && retryPolicy.retryOnTypeMismatch)
                                || (!validation.keyNameMismatched.isEmpty() && retryPolicy.retryOnKeyNameMismatch);
                if (!shouldRetry) break;
            }

            // 3. 仍需重试 → 构造重组 prompt 调用 LLM（最后一次不再调用）
            if (i >= retryPolicy.formatMaxRetries) break;

            String reformatPrompt = buildReformatPrompt(currentResponse, outputsDecl, lastError);
            String reformatted;
            try {
                reformatted = invokeReformatLlm(client, systemPrompt, reformatPrompt);
            } catch (Exception e) {
                log.warn("[StructuredOutput] 重组 LLM 异常 formatAttempt={}: {}",
                        i, e.getMessage());
                lastError = "重组 LLM 异常: " + e.getMessage();
                lastFailReason = FailReason.RUNTIME_ERROR;
                break;
            }
            reformatLlmCalls++;

            if (reformatted == null || reformatted.isBlank()) {
                lastError = "重组 LLM 返回空结果";
                lastFailReason = FailReason.PARSE_ERROR;
                break;
            }
            currentResponse = reformatted;
        }

        return ReformatOutcome.fail(
                lastError == null ? "格式重试耗尽" : lastError,
                lastFailReason == null ? FailReason.OTHER : lastFailReason,
                reformatLlmCalls);
    }

    private static boolean shouldRuntimeRetry(FailReason reason, RetryPolicy policy) {
        if (reason == null) return false;
        switch (reason) {
            case PARSE_ERROR:
                return policy.retryOnParseError;
            case MISSING_REQUIRED:
                return policy.retryOnMissingRequired;
            case TYPE_MISMATCH:
                return policy.retryOnTypeMismatch;
            case KEY_NAME_MISMATCH:
                return policy.retryOnKeyNameMismatch;
            case RUNTIME_ERROR:
                return true; // 重组调用本身失败，应升级到外层完整重试
            default:
                return false;
        }
    }

    private static FailReason classifyFailReason(OutputValidator.ValidationResult v) {
        if (!v.keyNameMismatched.isEmpty()) return FailReason.KEY_NAME_MISMATCH;
        if (!v.missingRequired.isEmpty()) return FailReason.MISSING_REQUIRED;
        if (!v.typeMismatched.isEmpty()) return FailReason.TYPE_MISMATCH;
        return FailReason.OTHER;
    }

    private static String buildValidationErrorMsg(OutputValidator.ValidationResult v) {
        return v.errorMessage == null ? "未知校验错误" : v.errorMessage;
    }

    // ========== 内部数据类 ==========

    /** 重组循环的输出。 */
    private static final class ReformatOutcome {
        final boolean ok;
        final Map<String, Object> data;
        final String errorMessage;
        final FailReason failReason;
        /** 实际触发的重组 LLM 调用次数（不含首次解析，因为首次解析用原响应不算 LLM 调用）。 */
        final int reformatLlmCalls;

        private ReformatOutcome(boolean ok, Map<String, Object> data, String errorMessage,
                                FailReason failReason, int reformatLlmCalls) {
            this.ok = ok;
            this.data = data;
            this.errorMessage = errorMessage;
            this.failReason = failReason;
            this.reformatLlmCalls = reformatLlmCalls;
        }

        static ReformatOutcome ok(Map<String, Object> data, int reformatLlmCalls) {
            return new ReformatOutcome(true, data, null, null, reformatLlmCalls);
        }

        static ReformatOutcome fail(String errorMessage, FailReason failReason, int reformatLlmCalls) {
            return new ReformatOutcome(false, Collections.emptyMap(),
                    errorMessage, failReason, reformatLlmCalls);
        }
    }

    /** 内层重试失败原因分类，用于决定是否升级到外层重试。 */
    private enum FailReason {
        PARSE_ERROR,           // JSON 解析失败
        MISSING_REQUIRED,      // 必填字段值为空
        TYPE_MISMATCH,         // 类型不匹配
        KEY_NAME_MISMATCH,     // 键名缺失（如返回中文键名）
        RUNTIME_ERROR,         // 重组 LLM 调用本身异常
        OTHER
    }

    // ========== LLM 调用 ==========

    /**
     * 完整推理调用：构造 userPrompt + JSON 模板指令，让 LLM 完整执行业务并产出 JSON。
     */
    private static String invokeFullLlm(ChatClient client, String userPrompt, String systemPrompt,
                                         JsonNode outputsDecl, String lastError) {
        String instruction = buildInstruction(outputsDecl, lastError);
        String fullPrompt = userPrompt + instruction;
        ChatClient.ChatClientRequestSpec req = client.prompt().user(fullPrompt);
        if (systemPrompt != null && !systemPrompt.isEmpty()) {
            req = req.system(systemPrompt);
        }
        return req.call().content();
    }

    /**
     * 重组调用：仅发送重组 prompt（不含原 userPrompt），让 LLM 根据上次响应重新组织 JSON 结构。
     * 不重新执行业务逻辑。
     */
    private static String invokeReformatLlm(ChatClient client, String systemPrompt, String reformatPrompt) {
        ChatClient.ChatClientRequestSpec req = client.prompt().user(reformatPrompt);
        if (systemPrompt != null && !systemPrompt.isEmpty()) {
            req = req.system(systemPrompt);
        }
        return req.call().content();
    }

    // ========== 指令构造 ==========

    /**
     * 构造结构化输出指令（追加到 userPrompt 末尾）。
     *
     * <p>模板格式：带类型示例值 + required 标记 + 扩展字段说明（snake_case 自由发挥）。</p>
     *
     * @param outputs   节点 outputs 声明数组
     * @param lastError 上一次的错误（外层重试时非 null），null 表示首次调用
     */
    public static String buildInstruction(JsonNode outputs, String lastError) {
        StringBuilder sb = new StringBuilder();
        sb.append("\n\n---\n**必须按如下 JSON 格式输出最终结果**（仅输出 JSON 本身，")
          .append("不要任何 Markdown 包裹、解释或额外文字）：\n\n");
        sb.append("{\n");
        for (JsonNode o : outputs) {
            String name = o.path("name").asText("");
            if (name.isEmpty()) continue;
            String type = o.path("type").asText("string");
            sb.append("  \"").append(name).append("\": ")
              .append(exampleValueFor(type)).append(",\n");
        }
        // 删除最后一个逗号
        int lastComma = sb.lastIndexOf(",\n");
        if (lastComma == sb.length() - 2) sb.deleteCharAt(lastComma);
        sb.append("}\n\n");

        // 字段说明（带 required 标记）
        sb.append("**字段说明**：\n");
        for (JsonNode o : outputs) {
            String name = o.path("name").asText("");
            if (name.isEmpty()) continue;
            String type = o.path("type").asText("string");
            boolean required = o.path("required").asBoolean(false);
            String desc = o.path("description").asText("");
            sb.append("- ").append(name).append(" (").append(type)
              .append(required ? ", required" : ", optional").append("): ")
              .append(desc.isEmpty() ? "（无描述）" : desc).append("\n");
        }

        // 扩展字段说明（LLM 自由发挥）
        sb.append("\n**扩展字段**（可选）：\n");
        sb.append("- 你可以自由添加业务必要的辅助字段（如 confidence、run_error、user_notice 等）\n");
        sb.append("- 命名规则：snake_case\n");
        sb.append("- 最小化原则：只放置真正必需的辅助信息，不要堆砌\n");
        sb.append("- 不要添加与上述已声明字段语义重叠的字段\n");

        // 约束
        sb.append("\n**约束**：\n");
        sb.append("- 必须严格使用上述声明的字段名，区分大小写、区分分隔符（禁止中文字段名）\n");
        sb.append("- required 字段缺失或类型不符将触发重试\n");
        sb.append("- 输出必须能被 JSON 直接解析（首尾不能有非 JSON 字符）\n");

        if (lastError != null && !lastError.isEmpty()) {
            sb.append("\n**上一次返回错误**：").append(lastError)
              .append("。请修正并重新输出符合上述 JSON 格式与字段名要求的结果。\n");
        }
        return sb.toString();
    }

    /**
     * 按声明类型返回 JSON 模板中的示例值。
     * 用于让 LLM 直观理解每种类型的输出形式。
     */
    private static String exampleValueFor(String type) {
        if (type == null) return "\"XXX\"";
        switch (type) {
            case "number": return "0";
            case "boolean": return "true";
            case "array": return "[\"item1\", \"item2\"]";
            case "object": return "{\"key\": \"value\"}";
            default: return "\"XXX\"";  // string / filePath / directoryPath / file 等
        }
    }

    /**
     * 构造重组 prompt：把上次响应 + JSON 模板 + 错误说明回喂给 LLM，让其仅重组 JSON 结构。
     *
     * <p><b>关键</b>：不包含原 userPrompt，避免重新触发业务逻辑推理。</p>
     *
     * @param previousResponse LLM 上一次的原始响应
     * @param outputsDecl      节点 outputs 声明
     * @param errorMessage     校验错误描述
     */
    public static String buildReformatPrompt(String previousResponse, JsonNode outputsDecl, String errorMessage) {
        StringBuilder sb = new StringBuilder();
        sb.append("你之前的响应：\n")
          .append(truncate(previousResponse, REFORMAT_RESPONSE_MAX_LEN))
          .append("\n\n---\n");
        sb.append("但上述响应未能通过结构校验，原因：")
          .append(errorMessage)
          .append("\n\n");
        sb.append("**重要**：请仅根据你之前响应中的语义内容，**重新组织**为符合下列字段名要求的 JSON。\n")
          .append("必须严格使用下列字段名（区分大小写、区分分隔符，禁止返回中文字段名），")
          .append("仅输出 JSON 本身，不要任何额外文字、解释、Markdown 代码块标记。\n\n");
        sb.append("{\n");
        for (JsonNode o : outputsDecl) {
            String name = o.path("name").asText("");
            if (name.isEmpty()) continue;
            String type = o.path("type").asText("string");
            sb.append("  \"").append(name).append("\": ")
              .append(exampleValueFor(type)).append(",\n");
        }
        int lastComma = sb.lastIndexOf(",\n");
        if (lastComma == sb.length() - 2) sb.deleteCharAt(lastComma);
        sb.append("}\n");
        return sb.toString();
    }

    private static String truncate(String s, int maxLen) {
        if (s == null) return "";
        return s.length() <= maxLen ? s : s.substring(0, maxLen) + "...（截断）";
    }

    // ========== 解析（保留 v1 逻辑） ==========

    /**
     * 解析 LLM 响应，按 outputs 拆分为 Map&lt;varName, value&gt;。
     *
     * <p><b>注意</b>：本方法只提取声明字段中实际存在的值。缺失或值为 null 的字段<b>不放入</b> Map，
     * 由 {@link OutputValidator} 的 strict 模式通过 {@code containsKey} 判定为键名缺失或值为空。</p>
     *
     * @param llmResponse LLM 返回的原始字符串
     * @param outputs     节点声明的 outputs 数组
     * @return 按变量名分桶的 Map；解析失败返回 null（调用方降级处理）
     */
    public static Map<String, Object> parse(String llmResponse, JsonNode outputs) {
        if (llmResponse == null || llmResponse.isBlank()) return null;
        if (outputs == null || !outputs.isArray() || outputs.isEmpty()) return null;

        JsonNode parsed = tryParseJson(llmResponse);
        if (parsed == null || !parsed.isObject()) return null;

        Map<String, Object> result = new LinkedHashMap<>();
        for (JsonNode o : outputs) {
            String name = o.path("name").asText("");
            if (name.isEmpty()) continue;
            JsonNode value = parsed.get(name);
            if (value == null || value.isNull()) {
                continue; // 不放入：让 strict 模式通过 !containsKey 判定为键名缺失或值为空
            }
            result.put(name, convertValue(value, o.path("type").asText("string")));
        }
        return result;
    }

    /**
     * 三级 JSON 提取：直接解析 → ```json``` 代码块 → 首尾{}子串。
     * 全部失败返回 null。
     */
    private static JsonNode tryParseJson(String text) {
        // 1. 直接解析
        try {
            return MAPPER.readTree(text);
        } catch (Exception ignored) {}

        // 2. 提取 ```json ... ```
        int start = text.indexOf("```json");
        if (start >= 0) {
            int end = text.indexOf("```", start + 7);
            if (end > start) {
                try {
                    return MAPPER.readTree(text.substring(start + 7, end).trim());
                } catch (Exception ignored) {}
            }
        }
        // 兼容普通 ``` 代码块（无 json 标记）
        int fenceStart = text.indexOf("```");
        if (fenceStart >= 0) {
            int contentStart = text.indexOf('\n', fenceStart);
            if (contentStart > 0) {
                int fenceEnd = text.indexOf("```", contentStart);
                if (fenceEnd > contentStart) {
                    try {
                        return MAPPER.readTree(text.substring(contentStart, fenceEnd).trim());
                    } catch (Exception ignored) {}
                }
            }
        }

        // 3. 提取首个 { 到末个 }
        int first = text.indexOf('{');
        int last = text.lastIndexOf('}');
        if (first >= 0 && last > first) {
            try {
                return MAPPER.readTree(text.substring(first, last + 1));
            } catch (Exception ignored) {}
        }
        return null;
    }

    /**
     * 按 outputs 声明类型转换值。
     * 类型转换失败保留字符串形式，并 WARN 日志。
     */
    private static Object convertValue(JsonNode value, String targetType) {
        if (value == null || value.isNull()) return null;
        try {
            return switch (targetType) {
                case "number" -> value.isNumber() ? value.numberValue() : Double.parseDouble(value.asText());
                case "boolean" -> value.isBoolean() ? value.booleanValue() : Boolean.parseBoolean(value.asText());
                case "array", "object" -> MAPPER.treeToValue(value, Object.class);
                default -> value.isTextual() ? value.asText() : value.toString();
            };
        } catch (Exception e) {
            log.warn("[StructuredOutput] 类型转换失败，保留原始字符串: target={}, err={}", targetType, e.getMessage());
            return value.toString();
        }
    }
}
