package com.agent.management.engine;

import com.agent.management.service.AiModelService;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;

import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * AGENTIC 模式全局决策者：每次访问完一个节点后，问 LLM "下一步访问哪个节点"。
 *
 * <p>对应设计文档 {@code docs/plans/agentic-workflow-plan.md} §2.2。
 * 与 {@code ConditionExecutor} 同款 LLM 调用套路，但 prompt 不同：
 * <ul>
 *   <li>输入：当前工作区变量 + 画布节点清单 + 已访问历史</li>
 *   <li>输出：JSON {@code {"next": "<nodeId>", "reason": "..."}}</li>
 * </ul>
 *
 * <p>失败处理：内部最多重试 1 次；仍失败抛异常，
 * 由 {@link AgenticExecutor} 捕获并走 {@link #fallbackToNextEdge}。</p>
 */
@Slf4j
@Service
public class PlannerService {

    private static final ObjectMapper MAPPER = new ObjectMapper();

    /** 工作区变量预览的最大条目数（避免 prompt 过长） */
    private static final int MAX_VAR_PREVIEW = 30;
    /** 单个变量值的最大长度（字符）。500 字符可覆盖节点 final_response 中"用户新需求"等关键信号 */
    private static final int MAX_VALUE_LEN = 500;
    /** 上一节点 output 摘要截断阈值（与 MAX_VALUE_LEN 解耦，因为 output 是核心信号） */
    private static final int MAX_LAST_OUTPUT_LEN = 800;
    /** LLM 重试次数（首次 + 1 次重试） */
    private static final int MAX_ATTEMPTS = 2;

    /**
     * 降级正则：从 Jackson 解析失败的 JSON 中提取 next 字段值。
     * <p>匹配 {@code "next": "..."}（容忍冒号两侧空白）。next 是节点 ID（字母、数字、-、_），
     * 不会含特殊字符，因此用字符类约束即可。</p>
     */
    private static final Pattern NEXT_FIELD_PATTERN =
            Pattern.compile("\"next\"\\s*:\\s*\"([A-Za-z0-9_-]+)\"");

    /**
     * 决定下一步访问哪个节点。
     *
     * @param dag                工作流 DAG
     * @param currentNodeId      刚执行完的节点 id
     * @param currentVars        当前工作区变量（含历史所有节点输出 + 初始输入）
     * @param nodeVisitCounter   各节点已访问次数（用于 prompt 中展示"已访问 N 次"，由 Planner 自主判断）
     * @param aiModelService     AI 模型服务（取 ChatClient）
     * @param fallbackBuilder    无可用模型时的兜底 builder
     * @param lastFailed         上一次执行（重试用尽后）是否最终失败。true 时 prompt 会带上失败原因
     * @param lastError          失败时的错误信息（lastFailed=true 时非空）
     * @param lastNodeOutput     刚执行完节点的 envelope 输出（截断到 MAX_LAST_OUTPUT_LEN 字符后注入 prompt，
     *                           让 Planner 直接看到节点 final_response 中的"用户新需求"等关键信号）。
     *                           可为 null（节点失败无 output 时）
     * @return 决策结果（next + reason）
     * @throws IllegalStateException 当 LLM 调用或解析连续失败时
     */
    public PlannerDecision decideNext(DagResolver.ResolvedDag dag, String currentNodeId,
                                      Map<String, Object> currentVars,
                                      Map<String, Integer> nodeVisitCounter,
                                      AiModelService aiModelService,
                                      ChatClient.Builder fallbackBuilder,
                                      boolean lastFailed, String lastError,
                                      Map<String, Object> lastNodeOutput) {
        String prompt = buildPrompt(dag, currentNodeId, currentVars, nodeVisitCounter,
                lastFailed, lastError, lastNodeOutput);

        Exception lastException = null;
        for (int attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
            try {
                String response = aiModelService.getChatClientOrFallback(fallbackBuilder)
                        .prompt()
                        .user(prompt)
                        .call()
                        .content();
                PlannerDecision decision = parseDecision(response);
                if (decision != null) {
                    log.info("[Planner] 节点 {} 决策：next={}, reason={}",
                            currentNodeId, decision.nextNodeId, decision.reason);
                    return decision;
                }
                log.warn("[Planner] 第 {} 次尝试 JSON 解析失败：{}", attempt, response);
            } catch (Exception e) {
                lastException = e;
                log.warn("[Planner] 第 {} 次调用失败：{}", attempt, e.getMessage());
            }
        }
        throw new IllegalStateException("Planner 决策失败：" + (lastException != null ? lastException.getMessage() : "解析失败"));
    }

    /**
     * 兜底：走画布上 currentNodeId 的第一条出边；
     * 没有出边则找画布上任意 output 节点（强制终结）；
     * 都没有则抛异常（无法收尾）。
     */
    public static PlannerDecision fallbackToNextEdge(DagResolver.ResolvedDag dag, String currentNodeId) {
        // 1. 优先走画布参考路径
        var outEdges = dag.getOutgoingEdges().get(currentNodeId);
        if (outEdges != null && !outEdges.isEmpty()) {
            String next = outEdges.get(0).getTarget();
            return new PlannerDecision(next, "Planner 失败，走画布参考路径");
        }
        // 2. 没有出边，强制找任意 output 节点
        for (Map.Entry<String, String> e : dag.getNodeTypeMap().entrySet()) {
            if ("output".equals(e.getValue())) {
                return new PlannerDecision(e.getKey(), "Planner 失败，强制结束到 output");
            }
        }
        // 3. 实在没有 output 节点，找画布上最后一个节点（按 levels 反向找）
        var levels = dag.getLevels();
        for (int i = levels.size() - 1; i >= 0; i--) {
            var level = levels.get(i);
            if (!level.isEmpty()) {
                return new PlannerDecision(level.get(level.size() - 1),
                        "Planner 失败，画布无 output 节点，强制收尾");
            }
        }
        throw new IllegalStateException("Planner 兜底失败：画布上无任何节点");
    }

    /* ============== 私有：prompt 构建与解析 ============== */

    private String buildPrompt(DagResolver.ResolvedDag dag, String currentNodeId,
                                Map<String, Object> currentVars,
                                Map<String, Integer> nodeVisitCounter,
                                boolean lastFailed, String lastError,
                                Map<String, Object> lastNodeOutput) {
        // 节点清单
        StringBuilder nodes = new StringBuilder();
        dag.getNodeDataMap().forEach((id, data) -> {
            String type = dag.getNodeTypeMap().get(id);
            String label = data.path("label").asText(id);
            int visited = nodeVisitCounter.getOrDefault(id, 0);
            nodes.append("- ").append(id).append(" (").append(type);
            if (!label.equals(id)) nodes.append("/").append(label);
            nodes.append(")");
            if (visited > 0) nodes.append(" [已访问 ").append(visited).append(" 次]");
            if ("output".equals(type)) nodes.append(" ← 终结节点");
            nodes.append("\n");
        });

        // 工作区变量预览
        String varsPreview = currentVars.entrySet().stream()
                .limit(MAX_VAR_PREVIEW)
                .map(e -> e.getKey() + ": " + truncate(String.valueOf(e.getValue()), MAX_VALUE_LEN))
                .collect(Collectors.joining("\n"));
        if (varsPreview.isEmpty()) varsPreview = "(无)";

        int currentNodeVisits = nodeVisitCounter.getOrDefault(currentNodeId, 0);

        // 上一节点 output 摘要（独立段落，让 Planner 直接看到节点 final_response 中的关键信号，
        // 如"用户有新的需求：XXX"，无需从 currentVars 反推）
        String lastOutputPreview;
        if (lastNodeOutput == null || lastNodeOutput.isEmpty()) {
            lastOutputPreview = "(节点无输出)";
        } else {
            try {
                String json = MAPPER.writeValueAsString(lastNodeOutput);
                lastOutputPreview = truncate(json, MAX_LAST_OUTPUT_LEN);
            } catch (Exception e) {
                lastOutputPreview = "(序列化失败)";
            }
        }

        // 失败上下文（重试用尽后仍失败时附加，提示 Planner 规避同样错误）
        String failureContext = "";
        if (lastFailed && lastError != null && !lastError.isBlank()) {
            failureContext = String.format("""

                    注意：当前节点本次执行最终失败（已重试用尽），失败原因：
                    %s

                    决策建议：
                    - 如果失败可由切换到其他节点绕过，请选择合适的下一个节点
                    - 如果当前节点仍有尝试价值，可以再次访问它（但请说明理由）
                    - 不要重复会触发同样错误的路径
                    """, truncate(lastError, 500));
        }

        return String.format("""
                你是工作流调度助手。请根据当前工作区状态决定下一步访问哪个节点。

                当前刚执行完：%s（已访问 %d 次）
                %s
                上一节点（%s）的输出：
                %s

                工作区变量（部分，按访问顺序累积，同名变量最近覆盖）：
                %s

                可访问节点清单：
                %s

                决策规则：
                1. 必须从上面清单中选择节点，不能创造新节点
                2. 选择 output 节点表示产出最终结果并结束运行
                3. 重访已执行过的节点是允许的，但必须基于具体理由。典型合理理由：
                   - 上一节点输出中出现"用户新需求""需要修正""请重做"等明确信号
                   - 编译/校验失败，需要重新生成
                   - 依赖数据缺失，需要补全
                   无理由的反复横跳（A→B→A→B→A...）才被压制
                4. 节点之间可以任意跳转，画布连线仅供参考，不构成约束

                请严格按以下 JSON 格式回复（不要添加任何额外说明）：
                {"next": "节点id", "reason": "50 字以内的理由"}

                格式硬性要求（违反将导致解析失败）：
                - reason 字段内禁止出现半角双引号（"）；如需引用，请用单引号（'）或书名号（《》），或直接转述不要原样引用
                - 例如，不要写 {"next":"A","reason":"输出含"用户新需求"信号"}（破坏 JSON 结构）
                  应改为 {"next":"A","reason":"输出含用户新需求信号"}
                """,
                currentNodeId,
                currentNodeVisits,
                failureContext,
                currentNodeId,
                lastOutputPreview,
                varsPreview,
                nodes.toString());
    }

    private PlannerDecision parseDecision(String response) {
        if (response == null) return null;
        // 容忍 LLM 加 ```json ``` 包裹
        String json = extractJsonBlock(response);
        try {
            JsonNode node = MAPPER.readTree(json);
            String next = node.path("next").asText("").trim();
            String reason = node.path("reason").asText("").trim();
            if (next.isEmpty()) return null;
            if (reason.isEmpty()) reason = "（未提供）";
            return new PlannerDecision(next, reason);
        } catch (Exception e) {
            // Jackson 严格解析失败时降级用正则提取 next（典型场景：LLM 在 reason 中
            // 嵌入未转义的半角双引号，如 "输出含"用户新需求"..."，破坏 JSON 结构）。
            // next 字段是节点 ID（不含特殊字符），单独提取可挽救决策。
            String nextFallback = extractNextByRegex(json);
            if (nextFallback != null) {
                log.warn("[Planner] JSON 解析失败，降级正则提取 next={}: {}",
                        nextFallback, truncate(response, 200));
                return new PlannerDecision(nextFallback, "（reason 解析失败）");
            }
            log.warn("[Planner] JSON 解析失败：{} | response={}", e.getMessage(),
                    truncate(response, 200));
            return null;
        }
    }

    /**
     * 降级提取 next 字段（仅 Jackson 严格解析失败时使用）。
     * <p>策略：匹配 {@code "next": "..."} 形式，next 值中不应出现嵌入双引号。
     * 容忍 next 与冒号之间的空白，以及值的单/双引号包裹。</p>
     */
    private String extractNextByRegex(String json) {
        if (json == null) return null;
        java.util.regex.Matcher m = NEXT_FIELD_PATTERN.matcher(json);
        if (m.find()) {
            String next = m.group(1).trim();
            return next.isEmpty() ? null : next;
        }
        return null;
    }

    /** 提取响应中的 JSON 块（容忍 ```json fence 与前后多余文字） */
    private String extractJsonBlock(String response) {
        String trimmed = response.trim();
        // 优先匹配 ```json ... ``` 或 ``` ... ```
        int fenceStart = trimmed.indexOf("```");
        if (fenceStart >= 0) {
            int contentStart = trimmed.indexOf('\n', fenceStart);
            int fenceEnd = trimmed.indexOf("```", contentStart >= 0 ? contentStart : fenceStart + 3);
            if (contentStart >= 0 && fenceEnd > contentStart) {
                return trimmed.substring(contentStart + 1, fenceEnd).trim();
            }
        }
        // 否则提取首个 { 到末尾 } 的部分
        int first = trimmed.indexOf('{');
        int last = trimmed.lastIndexOf('}');
        if (first >= 0 && last > first) {
            return trimmed.substring(first, last + 1);
        }
        return trimmed;
    }

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

    /**
     * 决策结果。
     * <p>{@code nextNodeId} 必须是画布节点清单中存在的 id；
     * 若 LLM 返回了不存在的 id，由 {@link AgenticExecutor} 兜底处理。</p>
     */
    @Data
    @AllArgsConstructor
    public static class PlannerDecision {
        private final String nextNodeId;
        private final String reason;

        public Map<String, Object> toMap() {
            Map<String, Object> m = new LinkedHashMap<>();
            m.put("next", nextNodeId);
            m.put("reason", reason);
            return Collections.unmodifiableMap(m);
        }
    }
}
