import type { OpenAiCompatibleChatOptions } from "@/lib/ai/openaiCompatibleChat";
import { openAiCompatibleChat, tryExtractJsonObject } from "@/lib/ai/openaiCompatibleChat";
import { DocumentBridgeError } from "@/lib/documents/bridge";
import {
  applyMindmapOps,
  classifyMindmapOpErrors,
  ensureMindmapUids,
  type MindmapOp,
  type MindmapTreeNode,
  type NodeRef,
} from "@/lib/mindmap/mindmapOps";
import { readMindmapLocal, writeMindmapLocal } from "@/lib/mindmap/mindmapLocalStore";
import { searchSearxng, type SearxResult } from "../searchWeb";

export type MindmapToolContext = {
  documentId: string;
  mindmapId: string;
  userId: string;
  // 仅用于生成更贴近当前选中节点的行为（可选）
  selectedUids?: string[];
  // 来自前端（@ 选择/上传）的附件列表，优先使用（避免额外查询）
  attachments?: Array<{ id: string; title: string; fileUrl: string; mimeType?: string | null }>;
};

type SupabaseRouteClient = {
  from: (table: string) => {
    select: (columns: string) => SupabaseQuery;
  };
};

type SupabaseQuery = {
  eq: (column: string, value: unknown) => SupabaseQuery;
  is: (column: string, value: unknown) => SupabaseQuery;
  maybeSingle: () => Promise<{ data: unknown; error: unknown }>;
};

export type MindmapSupabaseClient = SupabaseRouteClient;

type MindmapRustToolRunner = (input: {
  toolId: string;
  invocationKind: "query" | "command" | "job";
  toolArgs: Record<string, unknown>;
  data: MindmapTreeNode;
  target?: { workspaceId?: string | null; pageId?: string | null; blockId?: string | null } | null;
  reason?: string | null;
  refs?: string[];
}) => Promise<unknown>;

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 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 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 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 refsFromSearx = (r: SearxResult): NodeRef[] => [
  { kind: "url", fileUrl: r.url, title: r.title, snippet: r.snippet ? r.snippet.slice(0, 300) : undefined },
];

const coerceMimeKind = (mimeType: string) => {
  const m = String(mimeType || "").toLowerCase();
  if (m.includes("pdf")) return "pdf" as const;
  if (m.includes("word") || m.includes("docx")) return "docx" as const;
  if (m.includes("presentation") || m.includes("ppt")) return "pptx" as const;
  return "url" as const;
};

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

const resolveAttachmentFromContext = (ctx: MindmapToolContext, ref: string): ResolvedAttachment | null => {
  const s = String(ref || "").trim();
  if (!s) return null;
  const list = Array.isArray(ctx.attachments) ? ctx.attachments : [];
  const byId = list.find((a) => String(a.id) === s);
  if (byId) return { id: String(byId.id), title: String(byId.title || byId.id), fileUrl: String(byId.fileUrl || ""), mimeType: byId.mimeType ?? null };
  const exactTitle = list.find((a) => String(a.title) === s);
  if (exactTitle)
    return { id: String(exactTitle.id), title: String(exactTitle.title || exactTitle.id), fileUrl: String(exactTitle.fileUrl || ""), mimeType: exactTitle.mimeType ?? null };
  const fuzzy = list.find((a) => String(a.title || "").includes(s) || String(a.fileUrl || "").includes(s));
  if (fuzzy) return { id: String(fuzzy.id), title: String(fuzzy.title || fuzzy.id), fileUrl: String(fuzzy.fileUrl || ""), mimeType: fuzzy.mimeType ?? null };
  return null;
};

const mergeRefsUnique = (a: NodeRef[], b: NodeRef[]) => {
  const keyOf = (r: NodeRef) => `${r.kind}|${r.assetId ?? ""}|${r.fileUrl ?? ""}|${r.page ?? ""}|${r.slide ?? ""}`;
  const map = new Map<string, NodeRef>();
  for (const r of [...a, ...b]) {
    if (!r || typeof r !== "object") continue;
    map.set(keyOf(r), r);
  }
  return Array.from(map.values());
};

const throwMindmapOperationError = (
  message: string,
  kind: "rejected" | "failed",
  details?: Record<string, unknown>,
) => {
  if (kind === "rejected") {
    throw new DocumentBridgeError(message, 409, "REJECTED", {
      reason: "mindmap_rejected",
      ...details,
    });
  }
  throw new DocumentBridgeError(message, 500, "TRANSPORT_ERROR", {
    reason: "mindmap_failed",
    ...details,
  });
};

const sanitizeAddChildOps = (args: {
  targetUid: string;
  currentChildren: string[];
  ops: MindmapOp[];
  searxResults: SearxResult[];
}) => {
  const existed = new Set(args.currentChildren);
  const fixed: MindmapOp[] = [];
  for (const raw of args.ops) {
    if (!raw || typeof raw !== "object" || (raw as { op?: string }).op !== "addChild") continue;
    const parentUid = String((raw as { parentUid?: string }).parentUid ?? "");
    if (parentUid !== args.targetUid) continue;
    const node = (raw as { node?: Record<string, unknown> }).node ?? {};
    const textVal = String(node.text ?? "").trim();
    if (!textVal) continue;
    if (existed.has(textVal)) continue;
    existed.add(textVal);

    const href = safeUrlOrNull(node.hyperlink) ?? safeUrlOrNull(node.url) ?? null;
    const refs = Array.isArray(node.refs) ? (node.refs as NodeRef[]) : [];
    const hasRef = refs.some((x) => {
      if (!x || x.kind !== "url") return false;
      const fileUrl = typeof x.fileUrl === "string" ? x.fileUrl : "";
      return Boolean(safeUrlOrNull(fileUrl));
    });
    const finalRefs = hasRef ? refs : args.searxResults[0] ? refsFromSearx(args.searxResults[0]) : [];

    let finalText = textVal;
    if (!finalRefs.length && !/待核验/.test(finalText)) finalText = `${finalText}（待核验）`;

    fixed.push({
      op: "addChild",
      parentUid: args.targetUid,
      node: {
        text: finalText,
        ...(href ? { hyperlink: href } : {}),
        ...(finalRefs.length ? { refs: finalRefs } : {}),
      },
    });
    if (fixed.length >= 6) break;
  }

  if (!fixed.length && args.searxResults.length) {
    for (const r of args.searxResults.slice(0, 6)) {
      const title = String(r.title || "").trim();
      const url = safeUrlOrNull(r.url);
      if (!title || !url) continue;
      if (existed.has(title)) continue;
      existed.add(title);
      fixed.push({ op: "addChild", parentUid: args.targetUid, node: { text: title, hyperlink: url, refs: refsFromSearx(r) } });
      if (fixed.length >= 6) break;
    }
  }

  if (fixed.length < 3) {
    const base = args.currentChildren[0] ? "补完" : "补完";
    for (let i = fixed.length + 1; i <= 3; i += 1) {
      fixed.push({ op: "addChild", parentUid: args.targetUid, node: { text: `${base}（待核验）${i}` } });
    }
  }

  return fixed.slice(0, 6);
};

export const createMindmapServerTools = (args: {
  supabase?: SupabaseRouteClient;
  ctx: MindmapToolContext;
  cfg: OpenAiCompatibleChatOptions;
  allowedToolIds: Set<string>;
  runRustTool?: MindmapRustToolRunner;
  // 说明：Convex 迁移阶段用于“去 Supabase 化”。如果提供该能力，则完全不依赖 Supabase/local 文件。
  loadMindmap?: () => Promise<{
    doc: { id: string; title: string | null; workspace_id: string | null };
    base: MindmapTreeNode;
  }>;
  saveMindmap?: (args: {
    doc: { id: string; title: string | null; workspace_id: string | null };
    data: MindmapTreeNode;
  }) => Promise<void>;
}) => {
  const loadDoc = async () => {
    const { documentId, userId } = args.ctx;
    if (!args.supabase) {
      throw new Error("缺少 Supabase 数据源（请在 Convex 模式下传入 loadMindmap/saveMindmap）");
    }
    const query = args.supabase
      .from("documents")
      .select("id,title,workspace_id,mindmap_data")
      .eq("id", documentId)
      .eq("user_id", userId);
    const { data, error } = await query.maybeSingle();
    if (error && typeof error === "object" && "message" in (error as Record<string, unknown>)) {
      throw new Error(String((error as Record<string, unknown>).message ?? "读取页面失败"));
    }
    if (error) throw new Error("读取页面失败");
    if (!data) throw new Error("页面不存在");
    return data as { id: string; title: string | null; workspace_id: string | null; mindmap_data: unknown };
  };

  const loadMindmap = async () => {
    if (args.loadMindmap) {
      const loaded = await args.loadMindmap();
      ensureMindmapUids(loaded.base);
      return loaded;
    }
    const doc = await loadDoc();
    const local = await readMindmapLocal(args.ctx.documentId, args.ctx.mindmapId);
    const base = (local.ok ? local.data : (doc.mindmap_data ?? defaultMindmapData)) as MindmapTreeNode;
    ensureMindmapUids(base);
    return { doc, base };
  };

  const persistMindmap = async (doc: { id: string; title: string | null; workspace_id: string | null }, nextData: MindmapTreeNode) => {
    if (args.saveMindmap) {
      await args.saveMindmap({ doc, data: nextData });
      return;
    }
    await writeMindmapLocal(args.ctx.documentId, args.ctx.mindmapId, nextData, doc.title ?? "无标题");
  };

  const mindmapTarget = (doc: { id: string; title: string | null; workspace_id: string | null }) => ({
    pageId: doc.id,
    workspaceId: doc.workspace_id ?? null,
    blockId: args.ctx.mindmapId,
  });

  const withMindmapIds = (toolArgs: Record<string, unknown>) => ({
    ...toolArgs,
    documentId: args.ctx.documentId,
    mindmapId: args.ctx.mindmapId,
  });

  const persistResultData = async (doc: { id: string; title: string | null; workspace_id: string | null }, result: unknown) => {
    if (!result || typeof result !== "object" || Array.isArray(result)) return;
    const nextData = (result as Record<string, unknown>).data;
    if (!nextData || typeof nextData !== "object" || Array.isArray(nextData)) return;
    await persistMindmap(doc, nextData as MindmapTreeNode);
  };

  const mindmap_get = async (toolArgs: Record<string, unknown>) => {
    const maxNodes = Number(toolArgs.maxNodes ?? 120);
    const loaded = await loadMindmap();
    const { base } = loaded;
    if (args.runRustTool) {
      return await args.runRustTool({
        toolId: "mindmap_get",
        invocationKind: "query",
        toolArgs: withMindmapIds({ maxNodes }),
        data: base,
        target: mindmapTarget(loaded.doc),
      });
    }
    const list = walkSummaries(base, Number.isFinite(maxNodes) ? Math.max(10, Math.min(300, Math.floor(maxNodes))) : 120);
    return { ok: true, documentId: args.ctx.documentId, mindmapId: args.ctx.mindmapId, nodes: list };
  };

  const mindmap_get_subtree = async (toolArgs: Record<string, unknown>) => {
    const uid = String(toolArgs.uid ?? "").trim();
    if (!uid) throw new Error("缺少 uid");
    const depth = Number(toolArgs.depth ?? 2);
    const maxNodes = Number(toolArgs.maxNodes ?? 60);
    const loaded = await loadMindmap();
    const { base } = loaded;
    if (args.runRustTool) {
      return await args.runRustTool({
        toolId: "mindmap_get_subtree",
        invocationKind: "query",
        toolArgs: withMindmapIds({ uid, depth, maxNodes }),
        data: base,
        target: mindmapTarget(loaded.doc),
      });
    }
    const hit = findNodeByUid(base, uid);
    if (!hit) throw new Error(`未找到 uid=${uid}`);
    const list = summarizeSubtree(
      hit,
      Number.isFinite(depth) ? Math.max(0, Math.min(6, Math.floor(depth))) : 2,
      Number.isFinite(maxNodes) ? Math.max(5, Math.min(200, Math.floor(maxNodes))) : 60,
    );
    return { ok: true, uid, nodes: list };
  };

  const resolveMindmapPutTree = (toolArgs: Record<string, unknown>) => {
    const raw = toolArgs.data ?? toolArgs.tree ?? toolArgs.mindmap ?? null;
    if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
      throw new Error("缺少 data");
    }
    return raw as MindmapTreeNode;
  };

  const mindmap_put = async (toolArgs: Record<string, unknown>) => {
    const loaded = await loadMindmap();
    const data = resolveMindmapPutTree(toolArgs);
    if (args.runRustTool) {
      const result = await args.runRustTool({
        toolId: "mindmap_put",
        invocationKind: "command",
        toolArgs: withMindmapIds({ ...toolArgs, data }),
        data: loaded.base,
        target: mindmapTarget(loaded.doc),
      });
      await persistResultData(loaded.doc, result);
      return result;
    }
    ensureMindmapUids(data);
    await persistMindmap(loaded.doc, data);
    return { ok: true, data };
  };

  const mindmap_apply_ops = async (toolArgs: Record<string, unknown>) => {
    const ops = (toolArgs.ops ?? []) as unknown;
    const reason = String(toolArgs.reason ?? "").trim() || null;
    if (!Array.isArray(ops) || ops.length === 0) throw new Error("缺少 ops");
    if (ops.length > 80) throw new Error("ops 过多（最多 80）");

    const normalizeOp = (raw: unknown): MindmapOp | null => {
      if (!raw || typeof raw !== "object") return null;
      const o = raw as Record<string, unknown>;
      const op = String(o.op ?? "").trim();

      // 兼容历史/模型常见写法：update_node/add_child/set_link/append_note/delete_node
      if (op === "update_node" || op === "updateNode") {
        const uid = String(o.uid ?? o.id ?? "").trim();
        const text = String(o.text ?? o.value ?? "").trim();
        if (!uid || !text) return null;
        return { op: "updateText", uid, text };
      }
      if (op === "add_child" || op === "addChild") {
        const parentUid = String(o.parentUid ?? o.parent_uid ?? "").trim();
        const node = (o.node && typeof o.node === "object" ? (o.node as Record<string, unknown>) : {}) as Record<string, unknown>;
        const text = String(node.text ?? "").trim();
        if (!parentUid || !text) return null;
        const hyperlink = safeUrlOrNull(node.hyperlink) ?? null;
        const note = String(node.note ?? "").trim() || null;
        const refs = Array.isArray(node.refs) ? (node.refs as NodeRef[]) : [];
        return { op: "addChild", parentUid, node: { text, ...(hyperlink ? { hyperlink } : {}), ...(note ? { note } : {}), ...(refs.length ? { refs } : {}) } };
      }
      if (op === "set_link" || op === "set_hyperlink" || op === "setHyperlink") {
        const uid = String(o.uid ?? o.id ?? "").trim();
        const hyperlinkRaw = o.hyperlink ?? o.url ?? null;
        const hyperlink = hyperlinkRaw === null ? null : safeUrlOrNull(hyperlinkRaw);
        if (!uid) return null;
        return { op: "setHyperlink", uid, hyperlink: hyperlinkRaw === null ? null : hyperlink };
      }
      if (op === "append_note" || op === "appendNote") {
        const uid = String(o.uid ?? o.id ?? "").trim();
        const markdown = String(o.markdown ?? o.note ?? "").trim();
        if (!uid || !markdown) return null;
        return { op: "appendNote", uid, markdown };
      }
      if (op === "set_refs" || op === "setRefs") {
        const uid = String(o.uid ?? o.id ?? "").trim();
        const refs = Array.isArray(o.refs) ? (o.refs as NodeRef[]) : [];
        if (!uid || refs.length === 0) return null;
        return { op: "setRefs", uid, refs };
      }
      if (op === "delete_node" || op === "deleteNode") {
        const uid = String(o.uid ?? o.id ?? "").trim();
        if (!uid) return null;
        return { op: "deleteNode", uid };
      }

      // 原生协议（MindmapOp）
      if (
        op === "addChild" ||
        op === "addSiblingAfter" ||
        op === "updateText" ||
        op === "setHyperlink" ||
        op === "setRefs" ||
        op === "appendNote" ||
        op === "deleteNode"
      ) {
        return o as unknown as MindmapOp;
      }
      return null;
    };

    const normalized = ops.map((x) => normalizeOp(x)).filter(Boolean) as MindmapOp[];
    if (normalized.length === 0) throw new Error("ops 无有效操作（请使用 MindmapOp 协议或已支持的别名）");

    const loaded = await loadMindmap();
    const { base } = loaded;
    if (args.runRustTool) {
      const result = await args.runRustTool({
        toolId: "mindmap_apply_ops",
        invocationKind: "command",
        toolArgs: withMindmapIds({ ops: normalized, reason }),
        data: base,
        target: mindmapTarget(loaded.doc),
        reason,
      });
      await persistResultData(loaded.doc, result);
      return result;
    }
    const { data: nextData, applied, errors } = applyMindmapOps(base, normalized);
    if (errors.length > 0) {
      throwMindmapOperationError("mindmap_apply_ops 存在冲突或非法操作", classifyMindmapOpErrors(errors) ?? "failed", {
        errors,
        applied,
        opCount: normalized.length,
      });
    }
    await persistMindmap(loaded.doc, nextData);
    return { ok: true, applied, errors, data: nextData, meta: { reason } };
  };

  const mindmap_add_child = async (toolArgs: Record<string, unknown>) => {
    const parentUid = String(toolArgs.parentUid ?? "").trim();
    const text = String(toolArgs.text ?? "").trim();
    const hyperlink = safeUrlOrNull(toolArgs.hyperlink) ?? null;
    const note = String(toolArgs.note ?? "").trim() || null;
    const reason = String(toolArgs.reason ?? "").trim() || null;
    const refs = Array.isArray(toolArgs.refs) ? (toolArgs.refs as NodeRef[]) : [];
    if (!parentUid) throw new Error("缺少 parentUid");
    if (!text) throw new Error("缺少 text");

    const op: MindmapOp = {
      op: "addChild",
      parentUid,
      node: { text, ...(hyperlink ? { hyperlink } : {}), ...(note ? { note } : {}), ...(refs.length ? { refs } : {}) },
    };
    return await mindmap_apply_ops({ ops: [op], reason });
  };

  const mindmap_add_sibling_after = async (toolArgs: Record<string, unknown>) => {
    const targetUid = String(toolArgs.targetUid ?? "").trim();
    const text = String(toolArgs.text ?? "").trim();
    const hyperlink = safeUrlOrNull(toolArgs.hyperlink) ?? null;
    const note = String(toolArgs.note ?? "").trim() || null;
    const reason = String(toolArgs.reason ?? "").trim() || null;
    const refs = Array.isArray(toolArgs.refs) ? (toolArgs.refs as NodeRef[]) : [];
    if (!targetUid) throw new Error("缺少 targetUid");
    if (!text) throw new Error("缺少 text");

    const op: MindmapOp = {
      op: "addSiblingAfter",
      targetUid,
      node: { text, ...(hyperlink ? { hyperlink } : {}), ...(note ? { note } : {}), ...(refs.length ? { refs } : {}) },
    };
    return await mindmap_apply_ops({ ops: [op], reason });
  };

  const mindmap_update_node_text = async (toolArgs: Record<string, unknown>) => {
    const uid = String(toolArgs.uid ?? "").trim();
    const text = String(toolArgs.text ?? "").trim();
    const reason = String(toolArgs.reason ?? "").trim() || null;
    if (!uid) throw new Error("缺少 uid");
    if (!text) throw new Error("缺少 text");
    const op: MindmapOp = { op: "updateText", uid, text };
    return await mindmap_apply_ops({ ops: [op], reason });
  };

  const mindmap_set_hyperlink = async (toolArgs: Record<string, unknown>) => {
    const uid = String(toolArgs.uid ?? "").trim();
    const hyperlinkRaw = toolArgs.hyperlink;
    const hyperlink = hyperlinkRaw === null ? null : safeUrlOrNull(hyperlinkRaw);
    const reason = String(toolArgs.reason ?? "").trim() || null;
    if (!uid) throw new Error("缺少 uid");
    if (hyperlinkRaw !== null && hyperlinkRaw !== undefined && !hyperlink) {
      throw new Error("hyperlink 必须是 http(s) URL 或 null");
    }
    const op: MindmapOp = { op: "setHyperlink", uid, hyperlink: hyperlinkRaw === null ? null : hyperlink };
    return await mindmap_apply_ops({ ops: [op], reason });
  };

  const mindmap_append_note = async (toolArgs: Record<string, unknown>) => {
    const uid = String(toolArgs.uid ?? "").trim();
    const markdown = String(toolArgs.markdown ?? "").trim();
    const reason = String(toolArgs.reason ?? "").trim() || null;
    if (!uid) throw new Error("缺少 uid");
    if (!markdown) throw new Error("缺少 markdown");
    const op: MindmapOp = { op: "appendNote", uid, markdown };
    return await mindmap_apply_ops({ ops: [op], reason });
  };

  const mindmap_set_refs = async (toolArgs: Record<string, unknown>) => {
    const uid = String(toolArgs.uid ?? "").trim();
    const reason = String(toolArgs.reason ?? "").trim() || null;
    const refs = Array.isArray(toolArgs.refs) ? (toolArgs.refs as NodeRef[]) : [];
    if (!uid) throw new Error("缺少 uid");
    if (!refs.length) throw new Error("缺少 refs");
    const op: MindmapOp = { op: "setRefs", uid, refs };
    return await mindmap_apply_ops({ ops: [op], reason });
  };

  const mindmap_delete_node = async (toolArgs: Record<string, unknown>) => {
    const uid = String(toolArgs.uid ?? "").trim();
    const reason = String(toolArgs.reason ?? "").trim() || null;
    if (!uid) throw new Error("缺少 uid");
    const op: MindmapOp = { op: "deleteNode", uid };
    return await mindmap_apply_ops({ ops: [op], reason });
  };

  const mindmap_add_attachment_ref = async (toolArgs: Record<string, unknown>) => {
    const uid = String(toolArgs.uid ?? "").trim();
    const attachmentId = String(toolArgs.attachmentId ?? toolArgs.attachmentRef ?? "").trim();
    const fileUrlDirect = String(toolArgs.fileUrl ?? "").trim();
    const mode = String(toolArgs.mode ?? "append").trim() as "append" | "replace";
    const page = Number(toolArgs.page ?? 0);
    const slide = Number(toolArgs.slide ?? 0);
    const reason = String(toolArgs.reason ?? "").trim() || null;
    const snippet = String(toolArgs.snippet ?? "").trim() || null;
    if (!uid) throw new Error("缺少 uid");
    if (!attachmentId && !fileUrlDirect) throw new Error("缺少 attachmentId 或 fileUrl");

    const loaded = await loadMindmap();
    const { base } = loaded;
    const node = findNodeByUid(base, uid);
    if (!node) throw new Error(`未找到 uid=${uid}`);

    let resolved: ResolvedAttachment | null = null;
    if (attachmentId) resolved = resolveAttachmentFromContext(args.ctx, attachmentId);

    // 说明：Convex 模式下不再从 Supabase 二次查询附件；优先使用前端传入的 attachments 或调用方显式传入 fileUrl。
    if (!resolved && attachmentId && !fileUrlDirect) {
      throw new Error("未找到附件信息：请在调用工具时传入 fileUrl，或确保 attachments 包含该附件");
    }

    const finalUrl = (resolved?.fileUrl || fileUrlDirect || "").trim();
    const finalTitle = String(resolved?.title || toolArgs.title || attachmentId || "附件").trim();
    if (!finalUrl) throw new Error("附件缺少 fileUrl");

    const kindOverride = String(toolArgs.kind ?? "").trim();
    const kind =
      kindOverride === "pdf" || kindOverride === "docx" || kindOverride === "pptx" || kindOverride === "url"
        ? (kindOverride as NodeRef["kind"])
        : resolved?.mimeType
          ? coerceMimeKind(resolved.mimeType)
          : ("url" as const);

    const ref: NodeRef = {
      kind,
      ...(attachmentId ? { assetId: attachmentId } : {}),
      fileUrl: finalUrl,
      ...(Number.isFinite(page) && page > 0 ? { page: Math.floor(page) } : {}),
      ...(Number.isFinite(slide) && slide > 0 ? { slide: Math.floor(slide) } : {}),
      ...(finalTitle ? { title: finalTitle } : {}),
      ...(snippet ? { snippet } : {}),
    };

    const prevRefs = Array.isArray(node.data?.refs) ? (node.data.refs as NodeRef[]) : [];
    const nextRefs = mode === "replace" ? [ref] : mergeRefsUnique(prevRefs, [ref]);
    const op: MindmapOp = { op: "setRefs", uid, refs: nextRefs };
    return await mindmap_apply_ops({ ops: [op], reason });
  };

  const mindmap_add_attachment_child = async (toolArgs: Record<string, unknown>) => {
    const parentUid = String(toolArgs.parentUid ?? "").trim();
    const attachmentId = String(toolArgs.attachmentId ?? toolArgs.attachmentRef ?? "").trim();
    const fileUrlDirect = String(toolArgs.fileUrl ?? "").trim();
    const titleOverride = String(toolArgs.title ?? "").trim();
    const textOverride = String(toolArgs.text ?? "").trim();
    const note = String(toolArgs.note ?? "").trim() || null;
    const page = Number(toolArgs.page ?? 0);
    const slide = Number(toolArgs.slide ?? 0);
    const reason = String(toolArgs.reason ?? "").trim() || null;
    if (!parentUid) throw new Error("缺少 parentUid");
    if (!attachmentId && !fileUrlDirect) throw new Error("缺少 attachmentId 或 fileUrl");

    const resolved = attachmentId ? resolveAttachmentFromContext(args.ctx, attachmentId) : null;
    const finalUrl = String(resolved?.fileUrl || fileUrlDirect || "").trim();
    const finalTitle = String(titleOverride || resolved?.title || attachmentId || "附件").trim();
    if (!finalUrl) throw new Error("附件缺少 fileUrl");

    const kindOverride = String(toolArgs.kind ?? "").trim();
    const kind =
      kindOverride === "pdf" || kindOverride === "docx" || kindOverride === "pptx" || kindOverride === "url"
        ? (kindOverride as NodeRef["kind"])
        : resolved?.mimeType
          ? coerceMimeKind(resolved.mimeType)
          : ("url" as const);

    const parts: string[] = [];
    parts.push(textOverride || `附件：${finalTitle}`);
    if (Number.isFinite(page) && page > 0) parts.push(`p.${Math.floor(page)}`);
    if (Number.isFinite(slide) && slide > 0) parts.push(`slide ${Math.floor(slide)}`);
    const text = parts.join(" ");

    const ref: NodeRef = {
      kind,
      ...(attachmentId ? { assetId: attachmentId } : {}),
      fileUrl: finalUrl,
      ...(Number.isFinite(page) && page > 0 ? { page: Math.floor(page) } : {}),
      ...(Number.isFinite(slide) && slide > 0 ? { slide: Math.floor(slide) } : {}),
      ...(finalTitle ? { title: finalTitle } : {}),
    };

    const hyperlink = safeUrlOrNull(finalUrl) ?? null;
    const op: MindmapOp = {
      op: "addChild",
      parentUid,
      node: { text, ...(hyperlink ? { hyperlink } : {}), refs: [ref], ...(note ? { note } : {}) },
    };
    return await mindmap_apply_ops({ ops: [op], reason });
  };

  const mindmap_add_image_child = async (toolArgs: Record<string, unknown>) => {
    const parentUid = String(toolArgs.parentUid ?? "").trim();
    const imageRef = String(toolArgs.imageRef ?? toolArgs.imageId ?? toolArgs.imageUrl ?? "").trim();
    const alt = String(toolArgs.alt ?? "").trim() || "image";
    const caption = String(toolArgs.caption ?? "").trim() || "";
    const reason = String(toolArgs.reason ?? "").trim() || null;
    if (!parentUid) throw new Error("缺少 parentUid");
    if (!imageRef) throw new Error("缺少 imageRef（可用 attachmentId 或图片 URL）");

    const resolved = resolveAttachmentFromContext(args.ctx, imageRef);
    const url = String(resolved?.fileUrl || imageRef).trim();
    const title = String(resolved?.title || caption || "图片").trim();
    if (!url) throw new Error("图片缺少 URL");

    const markdown = `![${alt}](${url})`;
    const hyperlink = safeUrlOrNull(url) ?? null;
    const op: MindmapOp = {
      op: "addChild",
      parentUid,
      node: {
        text: caption ? caption : `图片：${title}`,
        ...(hyperlink ? { hyperlink } : {}),
        note: markdown,
        refs: [{ kind: "url", ...(resolved?.id ? { assetId: resolved.id } : {}), fileUrl: url, title }],
      },
    };
    return await mindmap_apply_ops({ ops: [op], reason });
  };

  const mindmap_append_image_note = async (toolArgs: Record<string, unknown>) => {
    const uid = String(toolArgs.uid ?? "").trim();
    const imageRef = String(toolArgs.imageRef ?? toolArgs.imageId ?? toolArgs.imageUrl ?? "").trim();
    const alt = String(toolArgs.alt ?? "").trim() || "image";
    const reason = String(toolArgs.reason ?? "").trim() || null;
    if (!uid) throw new Error("缺少 uid");
    if (!imageRef) throw new Error("缺少 imageRef（可用 attachmentId 或图片 URL）");

    const resolved = resolveAttachmentFromContext(args.ctx, imageRef);
    const url = String(resolved?.fileUrl || imageRef).trim();
    const title = String(resolved?.title || "图片").trim();
    if (!url) throw new Error("图片缺少 URL");

    const markdown = `![${alt}](${url})`;
    const op: MindmapOp = { op: "appendNote", uid, markdown };
    const result = await mindmap_apply_ops({ ops: [op], reason });
    if (result && typeof result === "object") {
      return {
        ...(result as Record<string, unknown>),
        meta: { reason, ref: { kind: "url", ...(resolved?.id ? { assetId: resolved.id } : {}), fileUrl: url, title } },
      };
    }
    return {
      ok: true,
      meta: { reason, ref: { kind: "url", ...(resolved?.id ? { assetId: resolved.id } : {}), fileUrl: url, title } },
      result,
    };
  };

  const mindmap_expand_node = async (toolArgs: Record<string, unknown>) => {
    const targetUid = String(toolArgs.targetUid ?? "").trim();
    if (!targetUid) throw new Error("缺少 targetUid");
    const instruction = String(toolArgs.instruction ?? "").trim();
    const useSearx = args.allowedToolIds.has("search_web");

    const loaded = await loadMindmap();
    const { base } = loaded;
    const target = findNodeByUid(base, targetUid);
    if (!target) throw new Error("未找到目标节点（uid 不存在）");

    const targetText = String(target?.data?.text ?? "").trim();
    const currentChildren = Array.isArray(target?.children)
      ? target.children
          .map((c) => String(c?.data?.text ?? "").trim())
          .filter(Boolean)
          .slice(0, 20)
      : [];

    const query = [targetText, instruction].filter(Boolean).join(" ").trim();
    const searxResults = useSearx && query ? await searchSearxng(query, 6).catch(() => []) : [];

    const system = [
      "你是一个“思维导图补完器”。",
      "你必须输出严格 JSON（不要 Markdown/不要代码块/不要解释）。",
      "你只能输出 {\"ops\": MindmapOp[]} 这一个对象。",
      "默认策略：为 targetUid 新增 3~6 个子节点（addChild）。",
      "每个新增节点必须提供 text，并尽量提供 hyperlink 与 refs（引用来自搜索结果 URL）。",
      "不要删除/改名已有节点；不要输出 addSiblingAfter/updateText/deleteNode。",
    ].join("\n");

    const user = [
      `documentId=${args.ctx.documentId}`,
      `mindmapId=${args.ctx.mindmapId}`,
      `targetUid=${targetUid}`,
      "",
      `目标节点：${targetText || "(empty)"}`,
      currentChildren.length ? `当前子节点（供去重）：${currentChildren.join("；")}` : "",
      instruction ? `用户要求：${instruction}` : "",
      "",
      "可用证据（搜索结果）：",
      ...(searxResults.length
        ? searxResults.map((r, idx) => {
            const snip = (r.snippet || "").replace(/\s+/g, " ").slice(0, 180);
            return `${idx + 1}. ${r.title}\n   url: ${r.url}\n   snippet: ${snip}`;
          })
        : ["(无)"]),
      "",
      "MindmapOp JSON Schema（仅供理解）：",
      '{ "ops": [ { "op": "addChild", "parentUid": string, "node": { "text": string, "hyperlink"?: string, "refs"?: NodeRef[] } } ] }',
      'NodeRef 示例：{ "kind": "url", "fileUrl": "https://example.com", "title": "来源标题", "snippet": "..." }',
      "",
      "硬性约束：",
      "- 仅输出 addChild；parentUid 必须等于 targetUid。",
      "- 新增节点 text 不要与当前子节点重复。",
      "- hyperlink 必须是 http(s) URL。",
      "- 每个新增节点 refs 至少 1 条（kind=url, fileUrl=来源url）；若没有来源，则在 text 末尾追加“（待核验）”。",
      "- 输出规模控制：最多 6 个节点。",
    ]
      .filter(Boolean)
      .join("\n");

    let ops: MindmapOp[] = [];
    let finishReason = "";
    try {
      const { text, raw } = await openAiCompatibleChat(
        [
          { role: "system", content: system },
          { role: "user", content: user },
        ],
        {
          ...args.cfg,
          timeoutMs: 40_000,
          maxTokens: 1800,
          maxCompletionTokens: 1800,
          responseFormat: "json_object",
        },
      );
      if (typeof raw === "object" && raw) {
        const r = raw as Record<string, unknown>;
        const choices = Array.isArray(r.choices) ? (r.choices as unknown[]) : [];
        const first = (choices[0] && typeof choices[0] === "object" ? (choices[0] as Record<string, unknown>) : null) ?? null;
        finishReason = first ? String(first.finish_reason ?? "") : "";
      }
      const json = tryExtractJsonObject(text);
      if (json && Array.isArray((json as Record<string, unknown>).ops)) ops = (json as Record<string, unknown>).ops as MindmapOp[];
    } catch {
      ops = [];
    }

    const fixed = sanitizeAddChildOps({ targetUid, currentChildren, ops, searxResults });
    const result = await mindmap_apply_ops({ ops: fixed, reason: instruction || null });
    if (result && typeof result === "object") {
      return {
        ...(result as Record<string, unknown>),
        ops: fixed,
        meta: { finishReason, searched: useSearx, searxCount: searxResults.length },
      };
    }
    return {
      ok: true,
      ops: fixed,
      meta: { finishReason, searched: useSearx, searxCount: searxResults.length },
      result,
    };
  };

  const run = async (toolId: string, toolArgs: Record<string, unknown>) => {
    if (toolId === "mindmap_get") return await mindmap_get(toolArgs);
    if (toolId === "mindmap_get_subtree") return await mindmap_get_subtree(toolArgs);
    if (toolId === "mindmap_put") return await mindmap_put(toolArgs);
    if (toolId === "mindmap_apply_ops") return await mindmap_apply_ops(toolArgs);
    if (toolId === "mindmap_add_child") return await mindmap_add_child(toolArgs);
    if (toolId === "mindmap_add_sibling_after") return await mindmap_add_sibling_after(toolArgs);
    if (toolId === "mindmap_update_node_text") return await mindmap_update_node_text(toolArgs);
    if (toolId === "mindmap_set_hyperlink") return await mindmap_set_hyperlink(toolArgs);
    if (toolId === "mindmap_append_note") return await mindmap_append_note(toolArgs);
    if (toolId === "mindmap_set_refs") return await mindmap_set_refs(toolArgs);
    if (toolId === "mindmap_delete_node") return await mindmap_delete_node(toolArgs);
    if (toolId === "mindmap_add_attachment_ref") return await mindmap_add_attachment_ref(toolArgs);
    if (toolId === "mindmap_add_attachment_child") return await mindmap_add_attachment_child(toolArgs);
    if (toolId === "mindmap_add_image_child") return await mindmap_add_image_child(toolArgs);
    if (toolId === "mindmap_append_image_note") return await mindmap_append_image_note(toolArgs);
    if (toolId === "mindmap_expand_node") return await mindmap_expand_node(toolArgs);
    throw new Error(`不支持的工具：${toolId}`);
  };

  return { run };
};
