import { NextResponse } from "next/server";
import { loadOnlineAiConfig } from "@/lib/ai/onlineAiConfig";
import { loadLocalAiConfig } from "@/lib/ai/localAiConfig";
import { openAiCompatibleChat, tryExtractJsonObject } from "@/lib/ai/openaiCompatibleChat";
import { buildDocumentBridgeContextWithActor } from "@/lib/documents/bridge";
import { executeRustBridgeTool } from "@/lib/documents/rust-runtime";
import { readMindmapLocal, writeMindmapLocal } from "@/lib/mindmap/mindmapLocalStore";

export const dynamic = "force-dynamic";

type AgentMessage = { role: "user" | "assistant"; content: string };

type AgentAttachment = {
  id: string;
  title: string;
  fileUrl: string;
  mimeType?: string | null;
};

type RequestPayload = {
  documentId: string;
  mindmapId: string;
  selectedUids?: string[];
  messages: AgentMessage[];
  attachments?: AgentAttachment[];
  toolChoice?: { mode: "auto" | "manual"; tools?: string[] };
  options?: { searxng?: boolean; ai?: { provider?: "online" | "local"; model?: string } };
};

type ToolName =
  | "mindmap_get"
  | "mindmap_get_subtree"
  | "search_web"
  | "mindmap_apply_ops"
  | "pdf_replace_mindmap";

type ToolCall =
  | { type: "tool"; tool: ToolName; args: Record<string, unknown> }
  | { type: "final"; message: string; summary?: string };

type SearxResult = { title: string; url: string; snippet?: string; engine?: string };

const defaultMindmapData: MindmapTreeNode = {
  data: { text: "中心主题" },
  children: [],
};

const safeUrlOrNull = (value: unknown) => {
  const s = typeof value === "string" ? value.trim() : "";
  if (!s) return null;
  try {
    const u = new URL(s);
    if (u.protocol !== "http:" && u.protocol !== "https:") return null;
    return u.toString();
  } catch {
    return null;
  }
};

const searchSearxng = async (q: string, count = 6): Promise<SearxResult[]> => {
  const base = (process.env.SEARXNG_BASE_URL ?? "http://127.0.0.1:8889").replace(/\/+$/, "");
  const token = (process.env.SEARXNG_API_TOKEN ?? "").trim();
  const url = `${base}/search?q=${encodeURIComponent(q)}&format=json&language=zh-CN&categories=general&safesearch=1`;

  const tryFetch = async (headers: Record<string, string>) => {
    const res = await fetch(url, { headers, method: "GET" });
    if (!res.ok) return null;
    return (await res.json().catch(() => null)) as any;
  };

  let json: any = null;
  if (token) {
    json = (await tryFetch({ Authorization: `Bearer ${token}` })) ?? (await tryFetch({ "X-API-Key": token })) ?? null;
  }
  if (!json) json = await tryFetch({});

  const results = Array.isArray(json?.results) ? json.results : [];
  return results
    .map((r: any) => ({
      title: String(r?.title ?? "").trim(),
      url: String(r?.url ?? "").trim(),
      snippet: String(r?.content ?? r?.snippet ?? "").trim(),
      engine: String(r?.engine ?? "").trim(),
    }))
    .filter((r: SearxResult) => r.title && safeUrlOrNull(r.url))
    .slice(0, Math.max(1, Math.min(10, count)));
};

const walkSummaries = (root: MindmapTreeNode, maxNodes = 120) => {
  const list: Array<{ uid: string; text: string; parentUid: string | null; depth: number; childCount: number }> = [];
  const queue: Array<{ node: MindmapTreeNode; parentUid: string | null; depth: number }> = [
    { node: root, parentUid: null, depth: 0 },
  ];

  while (queue.length && list.length < maxNodes) {
    const { node, parentUid, depth } = queue.shift()!;
    const uid = String(node?.data?.uid || "");
    const text = String(node?.data?.text ?? "").replace(/<[^>]+>/g, "").trim();
    const children = Array.isArray(node?.children) ? node.children : [];
    if (uid) {
      list.push({ uid, text, parentUid, depth, childCount: children.length });
    }
    children.forEach((c) => queue.push({ node: c, parentUid: uid || parentUid, depth: depth + 1 }));
  }
  return list;
};

const findNodeByUid = (root: MindmapTreeNode, uid: string): MindmapTreeNode | null => {
  const target = String(uid || "");
  if (!target) return null;
  const walk = (n: MindmapTreeNode): MindmapTreeNode | null => {
    if (String(n?.data?.uid || "") === target) return n;
    const children = Array.isArray(n.children) ? n.children : [];
    for (const c of children) {
      const hit = walk(c);
      if (hit) return hit;
    }
    return null;
  };
  return walk(root);
};

const summarizeSubtree = (node: MindmapTreeNode, depthLimit = 2, maxNodes = 60) => {
  const list: Array<{ uid: string; text: string; depth: number; childCount: number }> = [];
  const queue: Array<{ node: MindmapTreeNode; depth: number }> = [{ node, depth: 0 }];
  while (queue.length && list.length < maxNodes) {
    const { node: n, depth } = queue.shift()!;
    const uid = String(n?.data?.uid || "");
    const text = String(n?.data?.text ?? "").replace(/<[^>]+>/g, "").trim();
    const children = Array.isArray(n.children) ? n.children : [];
    if (uid) list.push({ uid, text, depth, childCount: children.length });
    if (depth < depthLimit) children.forEach((c) => queue.push({ node: c, depth: depth + 1 }));
  }
  return list;
};

const coerceToolCall = (raw: Record<string, unknown>): ToolCall | null => {
  const type = String((raw as any)?.type ?? "");
  if (type === "final") {
    return { type: "final", message: String((raw as any)?.message ?? ""), summary: String((raw as any)?.summary ?? "") || undefined };
  }
  if (type === "tool") {
    const tool = String((raw as any)?.tool ?? "") as ToolName;
    const args = ((raw as any)?.args ?? {}) as Record<string, unknown>;
    return { type: "tool", tool, args };
  }
  return null;
};

const normalizeAllowedTools = (payload: RequestPayload): Set<ToolName> => {
  const mode = payload.toolChoice?.mode ?? "auto";
  if (mode !== "manual") {
    return new Set<ToolName>(["mindmap_get", "mindmap_get_subtree", "search_web", "mindmap_apply_ops", "pdf_replace_mindmap"]);
  }
  const list = Array.isArray(payload.toolChoice?.tools) ? payload.toolChoice!.tools! : [];
  const allowed = new Set<ToolName>();
  for (const x of list) {
    const t = String(x || "") as ToolName;
    if (t === "mindmap_get" || t === "mindmap_get_subtree" || t === "search_web" || t === "mindmap_apply_ops" || t === "pdf_replace_mindmap") {
      allowed.add(t);
    }
  }
  // 手动模式但未选择：默认仍允许读导图（避免完全不可用）
  if (allowed.size === 0) allowed.add("mindmap_get");
  return allowed;
};

const refsFromSearx = (items: SearxResult[]): NodeRef[] =>
  items
    .map((r) => ({
      kind: "url" as const,
      fileUrl: r.url,
      title: r.title,
      snippet: r.snippet ? r.snippet.slice(0, 320) : undefined,
    }))
    .filter((x) => safeUrlOrNull(x.fileUrl));

const stripHtml = (value: unknown) => String(value ?? "").replace(/<[^>]+>/g, "").trim();

const stableStringify = (value: unknown): string => {
  if (value === null) return "null";
  const t = typeof value;
  if (t === "string") return JSON.stringify(value);
  if (t === "number" || t === "boolean") return String(value);
  if (Array.isArray(value)) return `[${value.map((x) => stableStringify(x)).join(",")}]`;
  if (t === "object") {
    const obj = value as Record<string, unknown>;
    const keys = Object.keys(obj).sort();
    return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",")}}`;
  }
  return JSON.stringify(String(value));
};

const formatSearchResults = (items: SearxResult[]) => {
  const lines: string[] = [];
  for (let i = 0; i < Math.min(8, items.length); i += 1) {
    const r = items[i];
    const title = String(r.title || "").trim();
    const url = String(r.url || "").trim();
    const snippet = String(r.snippet || "").replace(/\s+/g, " ").trim();
    lines.push(`${i + 1}. ${title || "(无标题)"}\n${url}${snippet ? `\n${snippet.slice(0, 240)}` : ""}`);
  }
  return lines.join("\n\n").trim();
};

const answerFromSearchResults = async (args: {
  cfg: { baseUrl: string; apiKey: string; model: string };
  modelOverride?: string | null;
  question: string;
  results: SearxResult[];
}) => {
  const q = String(args.question || "").trim();
  const resultsText = formatSearchResults(args.results);
  if (!q) return resultsText || "（无问题）";

  let aiError: string | null = null;
  try {
    const { text } = await openAiCompatibleChat(
      [
        {
          role: "system",
          content: [
            "你是一个严谨的中文助手。",
            "用户提出一个问题，你将基于提供的检索结果回答。",
            "要求：",
            "- 直接回答问题，给出结论；信息不确定时明确说明需要以官方为准。",
            "- 尽量在回答末尾附上 1-3 个最相关链接（原样 URL）。",
            "- 不要输出 JSON/不要输出 Markdown 代码块。",
          ].join("\n"),
        },
        { role: "user", content: `问题：${q}\n\n检索结果：\n${resultsText || "(无结果)"}` },
      ],
      {
        baseUrl: args.cfg.baseUrl,
        apiKey: args.cfg.apiKey,
        model: (args.modelOverride ?? "").trim() || args.cfg.model,
        timeoutMs: 55_000,
        maxTokens: 1200,
      },
    );
    const out = String(text ?? "").trim();
    if (out) return out;
  } catch (e) {
    aiError = e instanceof Error ? e.message : String(e);
  }

  const prefix = aiError ? `（在线 AI 总结失败：${aiError}）\n\n` : "";
  return `${prefix}${resultsText || "（未检索到结果）"}`.trim();
};

const buildFallbackExpandOps = async (args: {
  mindmap: MindmapTreeNode;
  targetUid: string;
  instruction: string;
  useSearx: boolean;
}): Promise<MindmapOp[]> => {
  const targetUid = String(args.targetUid || "").trim();
  const instruction = String(args.instruction || "").trim();
  const ops: MindmapOp[] = [];
  const seen = new Set<string>();

  const results = args.useSearx && instruction ? await searchSearxng(instruction, 6).catch(() => []) : [];
  for (const r of results.slice(0, 6)) {
    const title = String(r.title || "").trim();
    const url = safeUrlOrNull(r.url);
    const snippet = String(r.snippet || "").replace(/\s+/g, " ").trim();
    if (!title || !url) continue;
    const key = `${title}|${url}`;
    if (seen.has(key)) continue;
    seen.add(key);

    const explain = snippet ? snippet.slice(0, 46) : "相关资料（请核验）。";
    const text = `${title.slice(0, 20)}：${explain}`;
    ops.push({
      op: "addChild",
      parentUid: targetUid,
      node: {
        text,
        hyperlink: url,
        refs: [
          {
            kind: "url",
            fileUrl: url,
            title,
            snippet: snippet ? snippet.slice(0, 300) : undefined,
          },
        ],
      },
    });
    if (ops.length >= 6) break;
  }

  // 兜底：确保至少 3 条可见的“内容型”节点（不是纯链接）
  const base = instruction ? instruction.slice(0, 10) : stripHtml(args.mindmap?.data?.text) || "要点";
  let i = 1;
  while (ops.length < 3) {
    ops.push({
      op: "addChild",
      parentUid: targetUid,
      node: {
        text: `${base}（要点）${i}：请补充定义、条件、例子与注意事项。`,
      },
    });
    i += 1;
  }

  return ops;
};

export async function POST(request: Request) {
  const payload = (await request.json().catch(() => null)) as RequestPayload | null;
  if (!payload?.documentId || !payload?.mindmapId || !Array.isArray(payload.messages) || payload.messages.length === 0) {
    return NextResponse.json({ error: "缺少 documentId/mindmapId/messages" }, { status: 400 });
  }

  return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });

  /*
  const supabase = await createSupabaseRouteClient();
  const {
    data: { session },
  } = await supabase.auth.getSession();
  if (!session) {
    return NextResponse.json({ error: "未登录" }, { status: 401 });
  }

  const { data: doc, error: docError } = await supabase
    .from("documents")
    .select("id,title,workspace_id,mindmap_data")
    .eq("id", payload.documentId)
    .eq("user_id", session.user.id)
    .maybeSingle();
  if (docError) {
    return NextResponse.json({ error: docError.message }, { status: 400 });
  }
  if (!doc) {
    return NextResponse.json({ error: "页面不存在" }, { status: 404 });
  }

  const local = await readMindmapLocal(payload.documentId, payload.mindmapId);
  let mindmap = (local.ok ? local.data : (doc.mindmap_data ?? defaultMindmapData)) as MindmapTreeNode;
  // 补齐 uid，避免后续 addChild 找不到 root uid
  mindmap = applyMindmapOps(mindmap, []).data;

  const provider = payload.options?.ai?.provider === "local" ? "local" : "online";
  const modelOverride = String(payload.options?.ai?.model ?? "").trim() || null;
  const cfg =
    provider === "local"
      ? await loadLocalAiConfig().catch(() => null)
      : await loadOnlineAiConfig().catch(() => null);
  if (!cfg) {
    const tip =
      provider === "local"
        ? "未找到本地 AI 配置（LOCAL_AI_BASE_URL/LOCAL_AI_MODEL 或 ai.local.md / ai-local.md）"
        : "未找到在线 AI 配置（ai.md 或 ONLINE_AI_* 环境变量）";
    return NextResponse.json({ error: tip }, { status: 500 });
  }

  const allowed = normalizeAllowedTools(payload);
  const useSearx = payload.options?.searxng !== false;
  if (!useSearx) {
    allowed.delete("search_web");
  }

  const selected = Array.isArray(payload.selectedUids) ? payload.selectedUids.map((x) => String(x)).filter(Boolean).slice(0, 6) : [];
  const attachments = Array.isArray(payload.attachments) ? payload.attachments.slice(0, 10) : [];
  const attachmentLines = attachments
    .map((a, idx) => `${idx + 1}. id=${a.id} title=${a.title} mime=${a.mimeType ?? ""} url=${a.fileUrl}`)
    .join("\n");

  const summary = walkSummaries(mindmap, 120);
  const selectedSummaries = selected
    .map((uid) => {
      const hit = findNodeByUid(mindmap, uid);
      if (!hit) return `- uid=${uid} (not found)`;
      const t = String(hit.data?.text ?? "").replace(/<[^>]+>/g, "").trim();
      return `- uid=${uid} text=${t}`;
    })
    .join("\n");

  const toolSpec = [
    {
      name: "mindmap_get",
      desc: "读取整张导图的精简结构（uid+text+父子关系+子数量）。",
      args: { maxNodes: "number? (默认120)" },
    },
    {
      name: "mindmap_get_subtree",
      desc: "读取指定 uid 的子树（用于定位/精确改写）。",
      args: { uid: "string", depth: "number? (默认2)", maxNodes: "number? (默认60)" },
    },
    {
      name: "search_web",
      desc: "使用 SearxNG 搜索，返回标题/URL/摘要；用于提供可追溯来源。",
      args: { query: "string", count: "number? (默认6)" },
    },
    {
      name: "mindmap_apply_ops",
      desc: "对导图应用增量 ops（支持新增/删/改/加链接/加refs/加note），并自动落盘保存。",
      args: { ops: "MindmapOp[]", reason: "string? (可选)" },
    },
    {
      name: "pdf_replace_mindmap",
      desc: "从 PDF 生成“章->节->要点”的导图并替换当前导图（会保存）。fileRef 需匹配 attachments 里的 id 或标题。",
      args: { fileRef: "string", maxPages: "number? (默认9)" },
    },
  ] as const;

  const toolsText = toolSpec
    .filter((t) => allowed.has(t.name as ToolName))
    .map((t) => `- ${t.name}: ${t.desc} args=${JSON.stringify(t.args)}`)
    .join("\n");

  const system = [
    "你是“Mindmap AI Agent”。你可以像 CLI 工具一样，通过工具(API)完成任务：读导图、写导图、检索、从 PDF 生成导图。",
    "",
    "输出格式要求（硬性）：",
    "你必须输出严格 JSON（不要 Markdown/不要代码块/不要解释）。",
    "你只能输出以下两种之一：",
    '1) {"type":"tool","tool":ToolName,"args":{...}}',
    '2) {"type":"final","message":"...","summary":"...可选"}',
    "",
    "行为要求（硬性）：",
    "- 当用户明确要求“修改/生成/补完/写入导图”时，你必须至少调用一次 mindmap_apply_ops 或 pdf_replace_mindmap 来产生实际变更；不能只聊天。",
    "- 当用户只是“问问题/咨询”（未要求写入导图）时：你可以不改导图；若需要检索，最多调用 search_web 1 次，然后必须输出 final 直接回答；不要反复调用工具。",
    "- 新增节点优先写“内容”（可用 note 或子节点表达），并尽量带 refs（引用来源 URL 或 PDF 页码）。只有纯链接会显得呆板，应避免。",
    "- 若调用 search_web，必须把搜索结果转为 refs（NodeRef.kind=url），并用于新节点或 note。",
    "- ops 中的 uid 必须来自导图数据；不允许凭空捏造现有 uid。",
    "- 禁止重复调用同一个工具且参数完全相同。",
    "",
    "可用工具列表：",
    toolsText || "(无)",
  ].join("\n");

  const userIntro = [
    `documentId=${payload.documentId}`,
    `mindmapId=${payload.mindmapId}`,
    selected.length ? `selectedUids=${selected.join(",")}` : "selectedUids=(none)",
    "",
    "当前导图摘要（最多 120 个节点）：",
    JSON.stringify(summary, null, 2),
    "",
    selected.length ? `当前选中节点：\n${selectedSummaries}` : "",
    attachments.length ? `附件（可用 fileRef 引用）：\n${attachmentLines}` : "附件：(无)",
  ]
    .filter(Boolean)
    .join("\n");

  const conversation = payload.messages
    .slice(-10)
    .map((m) => ({ role: m.role, content: String(m.content ?? "") }));

  const maxSteps = 6;
  const trace: Array<{ step: number; call: ToolCall; toolResult?: unknown }> = [];
  let didMutate = false;
  const toolCounts = new Map<string, number>();

  const origin = new URL(request.url).origin;
  const cookie = request.headers.get("cookie") ?? "";

  const runPdfReplace = async (fileRef: string, maxPages: number) => {
    const ref = String(fileRef || "").trim();
    if (!ref) throw new Error("pdf_replace_mindmap: 缺少 fileRef");

    const attachment =
      attachments.find((a) => a.id === ref) ||
      attachments.find((a) => a.title === ref) ||
      attachments.find((a) => a.title?.includes(ref)) ||
      null;
    if (!attachment) throw new Error(`pdf_replace_mindmap: 未找到匹配附件：${ref}`);

    const fileUrl = String(attachment.fileUrl || "");
    const testName = (() => {
      try {
        const u = new URL(fileUrl, "http://local");
        if (u.pathname !== "/api/mindmap-ai/test-pdf") return null;
        const name = u.searchParams.get("name");
        return name ? decodeURIComponent(name) : null;
      } catch {
        return null;
      }
    })();
    const body =
      testName
        ? {
            source: { kind: "test", name: testName },
            options: { preferProvider: provider === "local" ? "ollama" : "online", maxPages },
          }
        : {
            source: { kind: "url", fileUrl },
            options: { preferProvider: provider === "local" ? "ollama" : "online", maxPages },
          };

    const res = await fetch(`${origin}/api/mindmap-ai/outline-to-mindmap`, {
      method: "POST",
      headers: { "Content-Type": "application/json", cookie },
      body: JSON.stringify(body),
    });
    const json = (await res.json().catch(() => null)) as any;
    if (!res.ok) {
      throw new Error(String(json?.error ?? `outline-to-mindmap 失败：${res.status}`));
    }
    if (!json?.mindmapData) {
      throw new Error("outline-to-mindmap 返回缺少 mindmapData");
    }
    mindmap = json.mindmapData as MindmapTreeNode;
    await writeMindmapLocal(payload.documentId, payload.mindmapId, mindmap, doc.title ?? "无标题");
    didMutate = true;
    return { ok: true, providerUsed: json?.meta?.providerUsed ?? "unknown", title: json?.meta?.title ?? "" };
  };

  const lastUser = [...payload.messages].reverse().find((m) => m.role === "user")?.content ?? "";
  const userIntentModify = /补完|写入|保存|生成|替换|总结|导图|节点/i.test(String(lastUser));
  const rootUid =
    String(mindmap?.data?.uid || "") ||
    String(walkSummaries(mindmap, 1)?.[0]?.uid || "");
  const defaultTargetUid = (selected[0] || rootUid || "").trim();

  // 纯问答模式：不要求改导图时，直接“检索 → 总结”返回，避免模型反复输出工具 JSON 导致卡步数
  if (!userIntentModify) {
    const question = String(lastUser || "").trim();
    const qaTrace: Array<{ step: number; call: ToolCall; toolResult?: unknown }> = [];

    if (question && allowed.has("search_web")) {
      const results = await searchSearxng(question, 6).catch(() => []);
      qaTrace.push({
        step: 1,
        call: { type: "tool", tool: "search_web", args: { query: question, count: 6 } },
        toolResult: results,
      });
      const answer = results.length
        ? await answerFromSearchResults({ cfg, modelOverride, question, results })
        : "（未检索到结果）";
      qaTrace.push({ step: 2, call: { type: "final", message: answer } });
      return NextResponse.json({ ok: true, message: answer, data: mindmap, trace: qaTrace });
    }

    // 未开启检索或无问题：直接用 AI 回答（不改导图）
    const { text } = await openAiCompatibleChat(
      [
        {
          role: "system",
          content: [
            "你是一个严谨的中文助手。",
            "用户提出问题，请直接回答。",
            "如果你不确定，明确说明并建议以官方为准。",
            "不要输出 JSON。",
          ].join("\n"),
        },
        { role: "user", content: question || "（空）" },
      ],
      {
        baseUrl: cfg.baseUrl,
        apiKey: cfg.apiKey,
        model: modelOverride || cfg.model,
        timeoutMs: 55_000,
        maxTokens: 1200,
      },
    );
    const answer = String(text || "").trim() || "（无输出）";
    qaTrace.push({ step: 1, call: { type: "final", message: answer } });
    return NextResponse.json({ ok: true, message: answer, data: mindmap, trace: qaTrace });
  }

  for (let step = 1; step <= maxSteps; step += 1) {
    const { text } = await openAiCompatibleChat(
      [
        { role: "system", content: system },
        { role: "user", content: userIntro },
        ...conversation,
        ...(trace.length
          ? [
              {
                role: "assistant" as const,
                content: `（工具执行历史摘要）\n${trace
                  .map((t) => `#${t.step} ${t.call.type === "tool" ? `tool=${t.call.tool}` : "final"} `)
                  .join("\n")}`,
              },
            ]
          : []),
      ],
      {
        baseUrl: cfg.baseUrl,
        apiKey: cfg.apiKey,
        model: modelOverride || cfg.model,
        timeoutMs: 55_000,
        maxTokens: 2200,
        maxCompletionTokens: 2200,
        responseFormat: "json_object",
      },
    );

    let json = tryExtractJsonObject(text);
    let call = json ? coerceToolCall(json) : null;
    // 若解析失败，再用更小上下文重试一次；仍失败则走兜底（保证可用性，不返回 500）
    if (!call) {
      try {
        const minimalSystem = [
          "你是 Mindmap AI Agent。",
          "你必须只输出一个 JSON：",
          '1) {"type":"tool","tool":ToolName,"args":{...}} 或 2) {"type":"final","message":"..."}',
          "",
          "可用工具：",
          toolsText || "(无)",
        ].join("\n");
        const minimalUser = [
          `documentId=${payload.documentId}`,
          `mindmapId=${payload.mindmapId}`,
          defaultTargetUid ? `targetUid=${defaultTargetUid}` : "",
          `用户请求：${String(lastUser).slice(0, 500)}`,
          "",
          "导图根节点：",
          JSON.stringify({ uid: rootUid, text: stripHtml(mindmap?.data?.text) }, null, 2),
        ]
          .filter(Boolean)
          .join("\n");
        const retry = await openAiCompatibleChat(
          [
            { role: "system", content: minimalSystem },
            { role: "user", content: minimalUser },
          ],
          {
            baseUrl: cfg.baseUrl,
            apiKey: cfg.apiKey,
            model: modelOverride || cfg.model,
            timeoutMs: 35_000,
            maxTokens: 900,
            maxCompletionTokens: 900,
            responseFormat: "json_object",
          },
        );
        json = tryExtractJsonObject(retry.text);
        call = json ? coerceToolCall(json) : null;
      } catch {
        call = null;
      }
    }

    if (!call) {
      // 兜底：若用户只是问问题，则不要擅自写入导图；优先给出基于检索的回答
      if (!userIntentModify) {
        const q = String(lastUser || "").trim();
        const results = allowed.has("search_web") && q ? await searchSearxng(q, 6).catch(() => []) : [];
        if (results.length) {
          trace.push({
            step,
            call: { type: "tool", tool: "search_web", args: { query: q, count: 6 } },
            toolResult: results,
          });
        }
        const answer = results.length
          ? await answerFromSearchResults({ cfg, modelOverride, question: q, results })
          : "AI 输出未能解析（未执行任何导图写入）。你可以：1) 开启联网检索；2) 换一个模型；3) 更明确说明要写入导图还是仅回答问题。";
        trace.push({ step, call: { type: "final", message: answer } });
        return NextResponse.json({ ok: true, message: answer, data: mindmap, trace });
      }

      // 用户明确要改导图：直接补完（写入并保存），避免用户看到“没做任何事”
      const ops = await buildFallbackExpandOps({
        mindmap,
        targetUid: defaultTargetUid || rootUid,
        instruction: String(lastUser || "补完选中节点"),
        useSearx: allowed.has("search_web"),
      });
      const { data: next, applied, errors } = applyMindmapOps(mindmap, ops);
      mindmap = next;
      await writeMindmapLocal(payload.documentId, payload.mindmapId, mindmap, doc.title ?? "无标题");
      didMutate = applied > 0 || didMutate;
      trace.push({
        step,
        call: { type: "tool", tool: "mindmap_apply_ops", args: { ops, reason: "fallback: ai-json-parse-failed" } },
        toolResult: { applied, errors, fallback: true },
      });
      return NextResponse.json({
        ok: true,
        message: "已补完并保存（AI 输出未能解析，已自动使用兜底策略确保可用）。",
        data: mindmap,
        trace,
      });
    }

    if (call.type === "final") {
      trace.push({ step, call });
      return NextResponse.json({
        ok: true,
        message: call.message,
        summary: call.summary ?? null,
        data: mindmap,
        trace,
      });
    }

    // 避免模型在同一个工具上打转（导致“已达最大执行步数”）
    const toolKey = `${call.tool}:${stableStringify(call.args)}`;
    const prevCount = toolCounts.get(toolKey) ?? 0;
    toolCounts.set(toolKey, prevCount + 1);
    if (prevCount >= 1) {
      trace.push({ step, call, toolResult: { warning: "检测到重复工具调用，已提前停止并返回结果。" } });
      const lastSearch = [...trace]
        .reverse()
        .find((t) => t.call.type === "tool" && t.call.tool === "search_web")?.toolResult as SearxResult[] | undefined;
      const answer =
        !userIntentModify && Array.isArray(lastSearch) && lastSearch.length
          ? await answerFromSearchResults({ cfg, modelOverride, question: String(lastUser || ""), results: lastSearch })
          : "检测到重复工具调用，已停止。你可以换一个模型或提供更明确的目标/限制。";
      trace.push({ step, call: { type: "final", message: answer } });
      return NextResponse.json({ ok: true, message: answer, data: mindmap, trace });
    }

    if (!allowed.has(call.tool)) {
      trace.push({ step, call, toolResult: { error: `工具未被允许：${call.tool}` } });
      continue;
    }

    try {
      if (call.tool === "mindmap_get") {
        const maxNodes = Number(call.args.maxNodes ?? 120) || 120;
        const result = walkSummaries(mindmap, Math.max(20, Math.min(300, maxNodes)));
        trace.push({ step, call, toolResult: result });
        continue;
      }

      if (call.tool === "mindmap_get_subtree") {
        const uid = String(call.args.uid ?? "");
        const depth = Number(call.args.depth ?? 2) || 2;
        const maxNodes = Number(call.args.maxNodes ?? 60) || 60;
        const hit = findNodeByUid(mindmap, uid);
        const result = hit ? summarizeSubtree(hit, Math.max(1, Math.min(6, depth)), Math.max(10, Math.min(200, maxNodes))) : null;
        trace.push({ step, call, toolResult: result });
        continue;
      }

      if (call.tool === "search_web") {
        const query = String(call.args.query ?? "").trim();
        const count = Number(call.args.count ?? 6) || 6;
        const result = query ? await searchSearxng(query, count) : [];
        trace.push({ step, call, toolResult: result });
        // 若用户只是问问题（非写导图），拿到检索结果后直接回答，避免继续 tool loop
        if (!userIntentModify && query && Array.isArray(result) && result.length) {
          const answer = await answerFromSearchResults({
            cfg,
            modelOverride,
            question: String(lastUser || query),
            results: result,
          });
          trace.push({ step, call: { type: "final", message: answer } });
          return NextResponse.json({ ok: true, message: answer, data: mindmap, trace });
        }
        continue;
      }

      if (call.tool === "mindmap_apply_ops") {
        const opsRaw = (call.args as any)?.ops;
        const ops = Array.isArray(opsRaw) ? (opsRaw as MindmapOp[]) : [];
        const safeOps = ops.filter((op) => op && typeof op === "object" && typeof (op as any).op === "string").slice(0, 80);

        // 若用户开启 searxng 且 ops 中缺 refs，可自动把最近一次 search_web 的结果补到 note/refs（避免纯链接/无来源）
        const lastSearch = [...trace]
          .reverse()
          .find((t) => t.call.type === "tool" && t.call.tool === "search_web")?.toolResult as SearxResult[] | undefined;
        const inferredRefs = Array.isArray(lastSearch) ? refsFromSearx(lastSearch).slice(0, 6) : [];
        const patchedOps: MindmapOp[] = safeOps.map((op) => {
          if ((op as any)?.op !== "addChild" && (op as any)?.op !== "addSiblingAfter") return op;
          const node = (op as any).node ?? {};
          const hasRefs = Array.isArray(node.refs) && node.refs.length > 0;
          if (hasRefs || inferredRefs.length === 0) return op;
          return {
            ...op,
            node: {
              ...node,
              refs: inferredRefs,
            },
          } as MindmapOp;
        });

        const { data: next, applied, errors } = applyMindmapOps(mindmap, patchedOps);
        mindmap = next;
        await writeMindmapLocal(payload.documentId, payload.mindmapId, mindmap, doc.title ?? "无标题");
        if (applied > 0) didMutate = true;
        trace.push({ step, call, toolResult: { applied, errors } });
        continue;
      }

      if (call.tool === "pdf_replace_mindmap") {
        const fileRef = String(call.args.fileRef ?? "").trim();
        const maxPages = Number(call.args.maxPages ?? 9) || 9;
        const result = await runPdfReplace(fileRef, Math.max(1, Math.min(40, maxPages)));
        trace.push({ step, call, toolResult: result });
        continue;
      }
    } catch (e) {
      trace.push({ step, call, toolResult: { error: e instanceof Error ? e.message : String(e) } });
      continue;
    }
  }

  if (!didMutate && userIntentModify && allowed.has("mindmap_apply_ops") && defaultTargetUid) {
    const ops = await buildFallbackExpandOps({
      mindmap,
      targetUid: defaultTargetUid,
      instruction: String(lastUser || "补完选中节点"),
      useSearx: allowed.has("search_web"),
    });
    const { data: next, applied, errors } = applyMindmapOps(mindmap, ops);
    mindmap = next;
    await writeMindmapLocal(payload.documentId, payload.mindmapId, mindmap, doc.title ?? "无标题");
    trace.push({
      step: maxSteps + 1,
      call: { type: "tool", tool: "mindmap_apply_ops", args: { ops, reason: "fallback: maxSteps-reached" } },
      toolResult: { applied, errors, fallback: true },
    });
    return NextResponse.json({
      ok: true,
      message: "已补完并保存（AI 未在限定步数内完成工具调用，已自动使用兜底策略）。",
      data: mindmap,
      trace,
    });
  }

  // 若用户只是问问题（不写导图），但模型卡在 tool loop：尽量基于最后一次检索结果给出回答
  const lastSearch = [...trace]
    .reverse()
    .find((t) => t.call.type === "tool" && t.call.tool === "search_web")?.toolResult as SearxResult[] | undefined;
  if (!userIntentModify && Array.isArray(lastSearch) && lastSearch.length) {
    const answer = await answerFromSearchResults({ cfg, modelOverride, question: String(lastUser || ""), results: lastSearch });
    trace.push({ step: maxSteps + 1, call: { type: "final", message: answer } });
    return NextResponse.json({ ok: true, message: answer, data: mindmap, trace }, { status: 200 });
  }

  return NextResponse.json(
    {
      ok: true,
      message: "已达到最大执行步数，已停止。你可以补充更明确的目标或限制。",
      data: mindmap,
      trace,
    },
    { status: 200 },
  );
  */
}
