/* eslint-disable @typescript-eslint/no-explicit-any */
import React, { useEffect, useMemo, useState } from "react";

// 说明：legacy/reference/compat 组件，仅作为 Phase 6 UI shell 迁移素材。
// 3000 文档页默认 mindmap 主链是 leptos-tiptap NodeView + Leptos/Rust shell。
import Image from "next/image";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Toggle } from "@/components/ui/toggle";
import { Bold, Italic, Underline, Type, Strikethrough, Palette, X, Network } from "lucide-react";
import {
  fontFamilyList,
  fontSizeList,
  lineHeightList,
  borderDasharrayList,
  borderRadiusList,
  borderWidthList,
  lineWidthList,
  shapeList,
  lineStyleList,
  themeList,
  layoutList,
  outlineDepthOptions,
  backgroundRepeatList,
  backgroundPositionList,
  backgroundSizeList,
} from "./mindmapOptions";
import iconConfig from "./mindmapIconConfig";
import imageConfig from "./mindmapImageConfig";
import { sidebarTitles, type SidebarPanel } from "./mindmapSidebarConfig";      
import type { MindMapNode } from "./mindmapTypes";
import { MindmapAiAgentPanel } from "./MindmapAiAgentPanel";
// 避免 SSR 阶段触发浏览器依赖，改为运行时动态引入
const loadIconModules = async () => {
  const { nodeIconList } = await import("simple-mind-map/src/svg/icons.js");
  const { mergerIconList } = await import("simple-mind-map/src/utils/index.js");
  return { nodeIconList, mergerIconList };
};

type SidebarProps = {
  documentId: string;
  mindmapId: string;
  mindmap: any;
  activeNodes: MindMapNode[];
  activeTab: SidebarPanel | null;
  onClose: () => void;
};

// 旧 Next route 已退场；补完节点能力保留在 AI Agent 的 mindmap_expand_node 工具中。
const MINDMAP_LEGACY_EXPAND_NODE_ENTRY_ENABLED = false;

const ColorInput = ({
  value,
  onChange,
  label,
}: {
  value: string;
  onChange: (val: string) => void;
  label?: string;
}) => (
  <div className="flex items-center gap-2">
    {label && <span className="text-xs text-gray-500">{label}</span>}
    <div className="relative h-6 w-full flex-1 overflow-hidden rounded border border-gray-200">
      <input
        type="color"
        value={value || "#000000"}
        onChange={(e) => onChange(e.target.value)}
        className="absolute inset-[-50%] h-[200%] w-[200%] cursor-pointer border-0 p-0"
      />
    </div>
  </div>
);

const NativeSelect = ({
  value,
  onChange,
  options,
  placeholder,
  disabled,
}: {
  value: string | number;
  onChange: (val: string) => void;
  options: { label: string | number; value: string | number }[];
  placeholder?: string;
  disabled?: boolean;
}) => (
  <div className="relative">
    <select
      disabled={disabled}
      value={value}
      onChange={(e) => onChange(e.target.value)}
      className="flex h-9 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 appearance-none"
    >
      {placeholder && <option value="" disabled>{placeholder}</option>}
      {options.map((opt) => (
        <option key={opt.value} value={opt.value}>
          {opt.label}
        </option>
      ))}
    </select>
  </div>
);

const StylePanel = ({ activeNodes }: { activeNodes: MindMapNode[] }) => {
  const [style, setStyle] = useState<Record<string, any>>({});

  useEffect(() => {
    if (activeNodes.length > 0) {
      const node = activeNodes[0];
      const newStyle: Record<string, any> = {};
      [
        "fontFamily", "fontSize", "lineHeight", "color", "fontWeight", "fontStyle",
        "textDecoration", "borderWidth", "borderColor", "fillColor", "shape",   
        "lineColor", "lineWidth", "lineDasharray", "borderRadius", "borderDasharray"
      ].forEach((prop) => {
        newStyle[prop] = node.getStyle(prop, false);
      });
      // 异步更新，避免同步 setState 抛 lint
      Promise.resolve().then(() => setStyle(newStyle));
    }
  }, [activeNodes]);

    const updateStyle = (prop: string, value: any) => {
      setStyle((prev) => ({ ...prev, [prop]: value }));
      activeNodes.forEach((node) => {
      node.setStyle?.(prop, value);
      });
    };

  if (activeNodes.length === 0) {
    return (
      <div className="flex h-full flex-col items-center justify-center p-4 text-center text-gray-400">
        <Type className="mb-2 h-10 w-10 opacity-20" />
        <p className="text-sm">请选择一个节点以编辑样式</p>
      </div>
    );
  }

  return (
    <div className="space-y-6">
      <div className="space-y-3">
        <Label className="text-xs font-semibold text-gray-500">文字</Label>
        <NativeSelect
          value={style.fontFamily || fontFamilyList[0]?.value}
          options={fontFamilyList.map((f) => ({ label: f.name, value: f.value }))}
          onChange={(v) => updateStyle("fontFamily", v)}
        />
        <div className="grid grid-cols-2 gap-2">
          <NativeSelect
            value={style.fontSize || 14}
            options={fontSizeList.map((s) => ({ label: `${s}px`, value: s }))}
            onChange={(v) => updateStyle("fontSize", Number(v))}
          />
          <NativeSelect
            value={style.lineHeight || 1.5}
            options={lineHeightList.map((h) => ({ label: h, value: h }))}
            onChange={(v) => updateStyle("lineHeight", Number(v))}
          />
        </div>
        <div className="flex items-center gap-2">
          <Toggle
            size="sm"
            pressed={style.fontWeight === "bold"}
            onPressedChange={(pressed) => updateStyle("fontWeight", pressed ? "bold" : "normal")}
          >
            <Bold className="h-4 w-4" />
          </Toggle>
          <Toggle
            size="sm"
            pressed={style.fontStyle === "italic"}
            onPressedChange={(pressed) => updateStyle("fontStyle", pressed ? "italic" : "normal")}
          >
            <Italic className="h-4 w-4" />
          </Toggle>
           <Toggle
            size="sm"
            pressed={style.textDecoration === "underline"}
            onPressedChange={(pressed) => updateStyle("textDecoration", pressed ? "underline" : "none")}
          >
            <Underline className="h-4 w-4" />
          </Toggle>
          <Toggle
            size="sm"
            pressed={style.textDecoration === "line-through"}
            onPressedChange={(pressed) => updateStyle("textDecoration", pressed ? "line-through" : "none")}
          >
            <Strikethrough className="h-4 w-4" />
          </Toggle>
        </div>
        <ColorInput
          label="颜色"
          value={style.color}
          onChange={(v) => updateStyle("color", v)}
        />
      </div>

      <div className="space-y-3">
        <Label className="text-xs font-semibold text-gray-500">边框 & 背景</Label>
        <div className="grid grid-cols-2 gap-2">
            <NativeSelect
               value={style.shape || "rectangle"}
               options={shapeList.map(s => ({label: s.name, value: s.value}))}
               onChange={(v) => updateStyle("shape", v)}
            />
            <NativeSelect
               value={style.borderDasharray || "none"}
               options={borderDasharrayList.map(s => ({label: s.name, value: s.value}))}
               onChange={(v) => updateStyle("borderDasharray", v)}
            />
        </div>
        <div className="grid grid-cols-2 gap-2">
             <NativeSelect
               value={Number.isFinite(style.borderWidth) ? style.borderWidth : (style.borderWidth ?? 0)}
               options={borderWidthList.map((n) => ({ label: `${n}px`, value: n }))}
               onChange={(v) => updateStyle("borderWidth", Number(v))}
             />
             <NativeSelect
               value={Number.isFinite(style.borderRadius) ? style.borderRadius : (style.borderRadius ?? 0)}
               options={borderRadiusList.map((n) => ({ label: `${n}px`, value: n }))}
               onChange={(v) => updateStyle("borderRadius", Number(v))}
             />
        </div>
        <div className="grid grid-cols-2 gap-2">
           <ColorInput
             label="边框"
             value={style.borderColor}
              onChange={(v) => updateStyle("borderColor", v)}
            />
            <ColorInput
              label="填充"
              value={style.fillColor}
              onChange={(v) => updateStyle("fillColor", v)}
            />
         </div>
      </div>

      <div className="space-y-3">
        <Label className="text-xs font-semibold text-gray-500">连线</Label>
        <div className="grid grid-cols-2 gap-2">
            <NativeSelect
               value={style.lineDasharray || "none"}
               options={borderDasharrayList.map(s => ({label: s.name, value: s.value}))}
               onChange={(v) => updateStyle("lineDasharray", v)}
            />
             <NativeSelect
               value={Number.isFinite(style.lineWidth) ? style.lineWidth : (style.lineWidth ?? 0)}
               options={lineWidthList.map((n) => ({ label: `${n}px`, value: n }))}
               onChange={(v) => updateStyle("lineWidth", Number(v))}
             />
        </div>
        <ColorInput
          label="连线颜色"
          value={style.lineColor}
          onChange={(v) => updateStyle("lineColor", v)}
        />
      </div>
    </div>
  );
};

const BaseStylePanel = ({ mindmap }: { mindmap: any }) => {
  const [style, setStyle] = useState<Record<string, any>>({});

  useEffect(() => {
    if (!mindmap) return;
    const initStyle = () => {
      const newStyle: Record<string, any> = {};
      [
        "backgroundColor", "lineColor", "lineWidth", "lineStyle",
        "paddingX", "paddingY",
        "backgroundImage", "backgroundRepeat", "backgroundPosition", "backgroundSize",
      ].forEach(key => {
        newStyle[key] = mindmap.getThemeConfig(key);
      });
      setStyle(newStyle);
    };
    
    initStyle();
  }, [mindmap]);

  const updateThemeConfig = (key: string, value: any) => {
    if (!mindmap) return;
    setStyle(prev => ({ ...prev, [key]: value }));
    mindmap.setThemeConfig({ [key]: value });
  };

  if (!mindmap) return null;

  return (
    <div className="space-y-6">
      <div className="space-y-3">
        <Label className="text-xs font-semibold text-gray-500">背景</Label>
        <ColorInput
          label="背景颜色"
          value={style.backgroundColor}
          onChange={(v) => updateThemeConfig("backgroundColor", v)}
        />
        <div className="space-y-2">
          <Label className="text-[11px] text-gray-500">背景图片</Label>
          <Input
            value={style.backgroundImage || ""}
            onChange={(e) => updateThemeConfig("backgroundImage", e.target.value || "")}
            placeholder="可粘贴图片地址"
          />
          <div className="grid grid-cols-3 gap-2 text-[12px] text-gray-600">
            <NativeSelect
              value={style.backgroundRepeat || "no-repeat"}
              options={backgroundRepeatList.map((i) => ({ label: i.name, value: i.value }))}
              onChange={(v) => updateThemeConfig("backgroundRepeat", v)}
            />
            <NativeSelect
              value={style.backgroundPosition || "center center"}
              options={backgroundPositionList.map((i) => ({ label: i.name, value: i.value }))}
              onChange={(v) => updateThemeConfig("backgroundPosition", v)}
            />
            <NativeSelect
              value={style.backgroundSize || "cover"}
              options={backgroundSizeList.map((i) => ({ label: i.name, value: i.value }))}
              onChange={(v) => updateThemeConfig("backgroundSize", v)}
            />
          </div>
        </div>
      </div>

      <div className="space-y-3">
        <Label className="text-xs font-semibold text-gray-500">连线 (默认)</Label>
        <div className="grid grid-cols-2 gap-2">
            <NativeSelect
              value={style.lineStyle || "curve"}
              options={lineStyleList.map(s => ({ label: s.name, value: s.value }))}
              onChange={(v) => updateThemeConfig("lineStyle", v)}
            />
             <NativeSelect
               value={Number.isFinite(style.lineWidth) ? style.lineWidth : (style.lineWidth ?? 0)}
               options={lineWidthList.map((n) => ({ label: `${n}px`, value: n }))}
               onChange={(v) => updateThemeConfig("lineWidth", Number(v))}
             />
        </div>
        <ColorInput
          label="连线颜色"
          value={style.lineColor}
          onChange={(v) => updateThemeConfig("lineColor", v)}
        />
      </div>

       <div className="space-y-3">
        <Label className="text-xs font-semibold text-gray-500">节点内边距 (默认)</Label>
        <div className="grid grid-cols-2 gap-2">
             <div className="flex items-center gap-2">
                 <span className="text-xs text-gray-500 whitespace-nowrap">水平</span>
                 <Input 
                    type="number" 
                    className="h-8" 
                    min={0}
                    value={parseInt(style.paddingX) || 0} 
                    onChange={e => updateThemeConfig("paddingX", Number(e.target.value))} 
                 />
             </div>
             <div className="flex items-center gap-2">
                 <span className="text-xs text-gray-500 whitespace-nowrap">垂直</span>
                 <Input 
                    type="number" 
                    className="h-8" 
                    min={0}
                    value={parseInt(style.paddingY) || 0} 
                    onChange={e => updateThemeConfig("paddingY", Number(e.target.value))} 
                 />
             </div>
        </div>
      </div>
    </div>
  );
};

const ThemePanel = ({ mindmap }: { mindmap: any }) => {
    const changeTheme = (theme: string) => {
        if (!mindmap) return;
        mindmap.setTheme(theme);
    };

    if (!mindmap) return null;

    return (
        <div className="grid grid-cols-2 gap-3">
            {themeList.map(t => (
                <button
                    key={t.value}
                    onClick={() => changeTheme(t.value)}
                    className="flex flex-col items-center justify-center rounded border border-gray-100 bg-gray-50 p-3 hover:border-blue-500 hover:bg-blue-50"
                >
                    <Palette className="mb-2 h-6 w-6 text-gray-400" />
                    <span className="text-xs text-gray-600">{t.name}</span>
                </button>
            ))}
        </div>
    )
}

const StructurePanel = ({ mindmap }: { mindmap: any }) => {
    const changeLayout = (layout: string) => {
        if (!mindmap) return;
        mindmap.setLayout(layout);
    };

    if (!mindmap) return null;

    return (
        <div className="grid grid-cols-2 gap-3">
            {layoutList.map(l => (
                <button
                    key={l.value}
                    onClick={() => changeLayout(l.value)}
                    className="flex flex-col items-center justify-center rounded border border-gray-100 bg-gray-50 p-3 hover:border-blue-500 hover:bg-blue-50"
                >
                    <Network className="mb-2 h-6 w-6 text-gray-400" />
                    <span className="text-xs text-gray-600">{l.name}</span>
                </button>
            ))}
        </div>
    )
}

const IconPanel = ({ activeNodes }: { activeNodes: MindMapNode[] }) => {        
  const [activeSubTab, setActiveSubTab] = useState<"icon" | "sticker">("icon");
  const [{ iconGroups, loading }, setIconGroups] = useState<{
    iconGroups: any[];
    loading: boolean;
  }>({ iconGroups: [], loading: true });
  useEffect(() => {
    let mounted = true;
    (async () => {
      const { nodeIconList, mergerIconList } = await loadIconModules();
      if (!mounted) return;
      setIconGroups({
        iconGroups: mergerIconList([
          ...nodeIconList,
          ...(iconConfig as unknown as any[]),
        ]),
        loading: false,
      });
    })();
    return () => {
      mounted = false;
    };
  }, []);
  const [stickerGroups] = useState(imageConfig);

  const addIcon = (type: string, name: string) => {
    const key = `${type}_${name}`;
    activeNodes.forEach((node) => {
      const rawIcons = node.getData("icon");
      const icons = Array.isArray(rawIcons) ? (rawIcons as string[]) : [];
      const newIcons = icons.filter((i) => !i.startsWith(`${type}_`));
      newIcons.push(key);
      node.setIcon?.(newIcons);
    });
  };

  const removeIcon = (type: string) => {
    activeNodes.forEach((node) => {
      const rawIcons = node.getData("icon");
      const icons = Array.isArray(rawIcons) ? (rawIcons as string[]) : [];
      const newIcons = icons.filter((i) => !i.startsWith(`${type}_`));
      node.setIcon?.(newIcons);
    });
  };

  const setSticker = (img: { url: string; width?: number; height?: number }) => {
    activeNodes.forEach((node) => {
      // simple-mind-map 支持 setImage 接收对象，包含 url/width/height
      node.setImage?.({
        url: img.url,
        width: img.width || 100,
        height: img.height || 100,
      });
    });
  };

  const clearSticker = () => {
    activeNodes.forEach((node) => {
      // 传入 null 以清除贴纸
      node.setImage?.(null);
    });
  };

  if (loading) return <div className="p-4 text-center text-gray-400">图标加载中…</div>;
  if (iconGroups.length === 0) return <div className="p-4 text-center text-gray-400">暂无图标</div>;
  if (activeNodes.length === 0) return <div className="p-4 text-center text-gray-400">请选择节点</div>;

  return (
    <div className="space-y-4 py-2">
      <div className="flex gap-2 px-1">
        <button
          className={`flex-1 rounded border px-2 py-1 text-xs ${activeSubTab === "icon" ? "border-blue-500 bg-blue-50 text-blue-600" : "border-gray-200"}`}
          onClick={() => setActiveSubTab("icon")}
        >
          图标
        </button>
        <button
          className={`flex-1 rounded border px-2 py-1 text-xs ${activeSubTab === "sticker" ? "border-blue-500 bg-blue-50 text-blue-600" : "border-gray-200"}`}
          onClick={() => setActiveSubTab("sticker")}
        >
          贴纸
        </button>
      </div>

      {activeSubTab === "icon" && iconGroups.map((group) => (
        <div key={group.type}>
          <div className="mb-2 flex items-center justify-between">
            <Label className="text-xs font-bold text-gray-600">{group.name}</Label>
            <button onClick={() => removeIcon(group.type)} className="text-[10px] text-blue-500 hover:underline">
              清除
            </button>
          </div>
          <div className="flex flex-wrap gap-2">
            {group.list.map((item: any) => (
              <button
                key={`${group.type}-${item.name}`}
                onClick={() => addIcon(group.type, item.name)}
                className="flex h-9 w-9 items-center justify-center rounded border border-gray-200 bg-white text-xs hover:border-blue-400 hover:shadow-sm overflow-hidden"
              >
                {item.icon ? (
                  typeof item.icon === "string" && item.icon.trim().startsWith("<svg") ? (
                    <span
                      className="inline-flex h-6 w-6 items-center justify-center overflow-hidden"
                      dangerouslySetInnerHTML={{ __html: item.icon }}
                    />
                  ) : (
                    <Image src={item.icon} alt={item.name} width={24} height={24} className="h-6 w-6 object-contain" />
                  )
                ) : (
                  <span className="text-xs text-gray-700">{item.name}</span>
                )}
              </button>
            ))}
          </div>
        </div>
      ))}

      {activeSubTab === "sticker" && (
        <div className="space-y-4">
          <div className="flex items-center justify-between px-1">
            <Label className="text-xs font-bold text-gray-600">贴纸</Label>
            <button onClick={clearSticker} className="text-[10px] text-blue-500 hover:underline">清除</button>
          </div>
          {stickerGroups.map((group: any) => (
            <div key={group.name}>
              <div className="mb-2 text-xs font-semibold text-gray-600">{group.name}</div>
              <div className="flex flex-wrap gap-2">
                {group.list.map((item: any, idx: number) => (
                  <button
                    key={`${group.name}-${idx}`}
                    onClick={() => setSticker(item)}
                    className="flex h-16 w-16 items-center justify-center rounded border border-gray-200 bg-white hover:border-blue-400 hover:shadow-sm"
                  >
                    <Image src={item.url} alt={group.name} width={56} height={56} className="h-14 w-14 object-contain" />
                  </button>
                ))}
              </div>
            </div>
          ))}
        </div>
      )}
    </div>
  );
};

const OutlinePanel = ({ mindmap }: { mindmap: any }) => {
  const [depth, setDepth] = useState<number>(-1);

  const nodes = useMemo(() => {
    const root = mindmap?.renderer?.renderTree?._node;
    const list: { node: any; text: string; depth: number }[] = [];
    const walk = (n: any, d: number) => {
      if (!n) return;
      const raw =
        n.nodeData?.data?.text ||
        n.nodeData?.text ||
        n.data?.text ||
        n.data?.data?.text ||
        "未命名";
      const text =
        typeof raw === "string"
          ? raw.replace(/<[^>]+>/g, "").trim() || "未命名"
          : "未命名";
      list.push({ node: n, text, depth: d });
      (n.children || []).forEach((c: any) => walk(c, d + 1));
    };
    if (root) walk(root, 0);
    return list;
  }, [mindmap]);

  const visibleNodes = useMemo(
    () => nodes.filter((item) => depth < 0 || item.depth < depth),
    [nodes, depth],
  );

  const activate = (node: any) => {
    const r = mindmap?.renderer;
    if (!node || !r) return;
    // 仅通过已有方法触发激活，避免直接改 renderer 属性
    if (typeof r.clearActiveNodeList === "function") {
      r.clearActiveNodeList();
    }
    if (typeof r.addNodeToActiveList === "function") {
      r.addNodeToActiveList(node, true);
    } else {
      // 兜底：仍保留最小副作用写入
      try {
        r?.setActiveNode?.(node);
      } catch {
        // 最后兜底：不再直接改引用，避免 lint 报错
      }
    }
    mindmap.emit?.("node_active", node, [node]);
    r.setRootNodeCenter?.();
  };

  if (!nodes.length) return <div className="p-4 text-sm text-gray-400">暂无节点</div>;

  return (
    <div className="space-y-3">
      <div>
        <Label className="text-xs text-gray-500">显示层级</Label>
        <NativeSelect
          value={depth}
          options={outlineDepthOptions.map((o) => ({ label: o.name, value: o.value }))}
          onChange={(v) => setDepth(Number(v))}
        />
      </div>
      <div className="max-h-[70vh] space-y-1 overflow-auto pr-2">
        {visibleNodes.map((item, idx) => (
          <button
            key={`${item.text}-${idx}`}
            className="flex w-full items-center gap-2 rounded-lg px-2 py-1 text-left text-sm hover:bg-gray-50"
            onClick={() => activate(item.node)}
          >
            <span className="text-gray-300" style={{ paddingLeft: item.depth * 12 }}>
              {"•".repeat(Math.max(1, item.depth + 1))}
            </span>
            <span className="text-gray-700">{item.text}</span>
          </button>
        ))}
      </div>
    </div>
  );
};

const SettingsPanel = ({ mindmap }: { mindmap: any }) => {
  const [config, setConfig] = useState({
    openPerformance: !!mindmap?.opt?.openPerformance,
    enableFreeDrag: !!mindmap?.opt?.enableFreeDrag,
    mousewheelAction: mindmap?.opt?.mousewheelAction || "zoom",
    mousewheelZoomActionReverse: !!mindmap?.opt?.mousewheelZoomActionReverse,
    openRealtimeRenderOnNodeTextEdit: !!mindmap?.opt?.openRealtimeRenderOnNodeTextEdit,
    alwaysShowExpandBtn: !!mindmap?.opt?.alwaysShowExpandBtn,
    enableAutoEnterTextEditWhenKeydown: !!mindmap?.opt?.enableAutoEnterTextEditWhenKeydown,
    createNewNodeBehavior: mindmap?.opt?.createNewNodeBehavior || "default",
    imgTextMargin: mindmap?.opt?.imgTextMargin ?? 5,
    textContentMargin: mindmap?.opt?.textContentMargin ?? 2,
  });
  const [aiOn, setAiOn] = useState<boolean>(mindmap?.opt?.enableAi ?? true);
  const [watermark, setWatermark] = useState({
    show: !!mindmap?.opt?.watermarkConfig?.text,
    onlyExport: mindmap?.opt?.watermarkConfig?.onlyExport ?? false,
    belowNode: mindmap?.opt?.watermarkConfig?.belowNode ?? false,
    text: mindmap?.opt?.watermarkConfig?.text ?? "",
    lineSpacing: mindmap?.opt?.watermarkConfig?.lineSpacing ?? 100,
    textSpacing: mindmap?.opt?.watermarkConfig?.textSpacing ?? 100,
    angle: mindmap?.opt?.watermarkConfig?.angle ?? 30,
    textStyle: {
      color: mindmap?.opt?.watermarkConfig?.textStyle?.color ?? "#999",
      opacity: mindmap?.opt?.watermarkConfig?.textStyle?.opacity ?? 0.5,
      fontSize: mindmap?.opt?.watermarkConfig?.textStyle?.fontSize ?? 14,
    },
  });

  useEffect(() => {
    const opt = mindmap?.opt || {};
    Promise.resolve().then(() =>
      setConfig({
        openPerformance: !!opt.openPerformance,
        enableFreeDrag: !!opt.enableFreeDrag,
        mousewheelAction: opt.mousewheelAction || "zoom",
        mousewheelZoomActionReverse: !!opt.mousewheelZoomActionReverse,
        openRealtimeRenderOnNodeTextEdit: !!opt.openRealtimeRenderOnNodeTextEdit,
        alwaysShowExpandBtn: !!opt.alwaysShowExpandBtn,
        enableAutoEnterTextEditWhenKeydown: !!opt.enableAutoEnterTextEditWhenKeydown,
        createNewNodeBehavior: opt.createNewNodeBehavior || "default",
        imgTextMargin: opt.imgTextMargin ?? 5,
        textContentMargin: opt.textContentMargin ?? 2,
      }),
    );
    const wm = opt.watermarkConfig || {};
    Promise.resolve().then(() =>
      setWatermark({
        show: !!wm.text,
        onlyExport: wm.onlyExport ?? false,
        belowNode: wm.belowNode ?? false,
        text: wm.text ?? "",
        lineSpacing: wm.lineSpacing ?? 100,
        textSpacing: wm.textSpacing ?? 100,
        angle: wm.angle ?? 30,
        textStyle: {
          color: wm.textStyle?.color ?? "#999",
          opacity: wm.textStyle?.opacity ?? 0.5,
          fontSize: wm.textStyle?.fontSize ?? 14,
        },
      }),
    );
    Promise.resolve().then(() => setAiOn(opt.enableAi ?? true));
  }, [mindmap]);

  const updateOpt = (key: string, value: any, needRender = false) => {
    setConfig((prev) => ({ ...prev, [key]: value }));
    mindmap?.updateConfig?.({ [key]: value });
    if (needRender && mindmap?.reRender) {
      mindmap.reRender();
    }
  };

  const applyWatermark = (next: any) => {
    const { show, ...cfg } = next;
    const finalCfg = show ? cfg : { ...cfg, text: "" };
    mindmap?.watermark?.updateWatermark?.(finalCfg);
    mindmap?.updateConfig?.({ watermarkConfig: finalCfg });
  };

  const updateWatermark = (patch: any) => {
    setWatermark((prev) => {
      const next = {
        ...prev,
        ...patch,
        textStyle: { ...prev.textStyle, ...(patch.textStyle || {}) },
      };
      applyWatermark(next);
      return next;
    });
  };

  const updateAi = (val: boolean) => {
    setAiOn(val);
    mindmap?.updateConfig?.({ enableAi: val });
  };

  return (
    <div className="space-y-6">
      <div className="space-y-3">
        <div className="flex items-center justify-between">
          <Label className="text-xs text-gray-500">显示水印</Label>
          <Toggle
            size="sm"
            pressed={watermark.show}
            onPressedChange={(v) => updateWatermark({ show: v })}
          >
            {watermark.show ? "开" : "关"}
          </Toggle>
        </div>
        {watermark.show && (
          <div className="space-y-3 rounded-md border border-gray-100 p-3">
            <div className="flex items-center justify-between">
              <Label className="text-xs text-gray-500">仅导出时显示</Label>
              <Toggle
                size="sm"
                pressed={watermark.onlyExport}
                onPressedChange={(v) => updateWatermark({ onlyExport: v })}
              >
                {watermark.onlyExport ? "开" : "关"}
              </Toggle>
            </div>
            <div className="flex items-center justify-between">
              <Label className="text-xs text-gray-500">节点下方</Label>
              <Toggle
                size="sm"
                pressed={watermark.belowNode}
                onPressedChange={(v) => updateWatermark({ belowNode: v })}
              >
                {watermark.belowNode ? "开" : "关"}
              </Toggle>
            </div>
            <div className="space-y-2">
              <Label className="text-xs text-gray-500">水印文字</Label>
              <Input
                value={watermark.text}
                placeholder="请输入水印文字"
                onChange={(e) => updateWatermark({ text: e.target.value })}
              />
            </div>
            <div className="grid grid-cols-2 gap-3">
              <ColorInput
                label="颜色"
                value={watermark.textStyle.color}
                onChange={(val) => updateWatermark({ textStyle: { color: val } })}
              />
              <div className="space-y-1">
                <Label className="text-xs text-gray-500">透明度 0~1</Label>
                <Input
                  type="number"
                  min={0}
                  max={1}
                  step={0.1}
                  value={watermark.textStyle.opacity}
                  onChange={(e) =>
                    updateWatermark({
                      textStyle: {
                        opacity: Math.min(1, Math.max(0, Number(e.target.value) || 0)),
                      },
                    })
                  }
                />
              </div>
              <div className="space-y-1">
                <Label className="text-xs text-gray-500">字号</Label>
                <Input
                  type="number"
                  min={8}
                  max={50}
                  step={1}
                  value={watermark.textStyle.fontSize}
                  onChange={(e) =>
                    updateWatermark({
                      textStyle: { fontSize: Number(e.target.value) || 14 },
                    })
                  }
                />
              </div>
              <div className="space-y-1">
                <Label className="text-xs text-gray-500">旋转角度</Label>
                <Input
                  type="number"
                  min={0}
                  max={90}
                  step={5}
                  value={watermark.angle}
                  onChange={(e) => updateWatermark({ angle: Number(e.target.value) || 0 })}
                />
              </div>
              <div className="space-y-1">
                <Label className="text-xs text-gray-500">行间距</Label>
                <Input
                  type="number"
                  min={10}
                  step={10}
                  value={watermark.lineSpacing}
                  onChange={(e) => updateWatermark({ lineSpacing: Number(e.target.value) || 0 })}
                />
              </div>
              <div className="space-y-1">
                <Label className="text-xs text-gray-500">字间距</Label>
                <Input
                  type="number"
                  min={10}
                  step={10}
                  value={watermark.textSpacing}
                  onChange={(e) => updateWatermark({ textSpacing: Number(e.target.value) || 0 })}
                />
              </div>
            </div>
          </div>
        )}
      </div>

      <div className="space-y-3">
        <div className="flex items-center justify-between">
          <Label className="text-xs text-gray-500">性能模式</Label>
          <Toggle
            size="sm"
            pressed={config.openPerformance}
            onPressedChange={(v) => updateOpt("openPerformance", v)}
          >
            {config.openPerformance ? "开" : "关"}
          </Toggle>
        </div>
        <div className="flex items-center justify-between">
          <Label className="text-xs text-gray-500">自由拖拽</Label>
          <Toggle
            size="sm"
            pressed={config.enableFreeDrag}
            onPressedChange={(v) => updateOpt("enableFreeDrag", v)}
          >
            {config.enableFreeDrag ? "开" : "关"}
          </Toggle>
        </div>
        <div className="flex items-center justify-between">
          <Label className="text-xs text-gray-500">实时渲染（编辑时）</Label>
          <Toggle
            size="sm"
            pressed={config.openRealtimeRenderOnNodeTextEdit}
            onPressedChange={(v) => updateOpt("openRealtimeRenderOnNodeTextEdit", v)}
          >
            {config.openRealtimeRenderOnNodeTextEdit ? "开" : "关"}
          </Toggle>
        </div>
        <div className="flex items-center justify-between">
          <Label className="text-xs text-gray-500">展开按钮常显</Label>
          <Toggle
            size="sm"
            pressed={config.alwaysShowExpandBtn}
            onPressedChange={(v) => updateOpt("alwaysShowExpandBtn", v, true)}
          >
            {config.alwaysShowExpandBtn ? "开" : "关"}
          </Toggle>
        </div>
        <div className="flex items-center justify-between">
          <Label className="text-xs text-gray-500">键入自动进入编辑</Label>
          <Toggle
            size="sm"
            pressed={config.enableAutoEnterTextEditWhenKeydown}
            onPressedChange={(v) => updateOpt("enableAutoEnterTextEditWhenKeydown", v)}
          >
            {config.enableAutoEnterTextEditWhenKeydown ? "开" : "关"}
          </Toggle>
        </div>
        <div className="space-y-2">
          <Label className="text-xs text-gray-500">滚轮行为</Label>
          <NativeSelect
            value={config.mousewheelAction}
            onChange={(v) => updateOpt("mousewheelAction", v)}
            options={[
              { label: "缩放", value: "zoom" },
              { label: "平移", value: "move" },
            ]}
          />
        </div>
        {config.mousewheelAction === "zoom" && (
          <div className="space-y-2">
            <Label className="text-xs text-gray-500">缩放方向反转</Label>
            <NativeSelect
              value={String(config.mousewheelZoomActionReverse)}
              onChange={(v) => updateOpt("mousewheelZoomActionReverse", v === "true")}
              options={[
                { label: "常规", value: "false" },
                { label: "反转", value: "true" },
              ]}
            />
          </div>
        )}
        <div className="space-y-2">
          <Label className="text-xs text-gray-500">创建新节点激活行为</Label>
          <NativeSelect
            value={config.createNewNodeBehavior}
            onChange={(v) => updateOpt("createNewNodeBehavior", v)}
            options={[
              { label: "默认", value: "default" },
              { label: "不激活", value: "notActive" },
              { label: "仅激活新节点", value: "activeOnly" },
            ]}
          />
        </div>
        <div className="grid grid-cols-2 gap-3">
          <div className="space-y-1">
            <Label className="text-xs text-gray-500">图片与文本间距</Label>
            <Input
              type="number"
              min={0}
              step={1}
              value={config.imgTextMargin}
              onChange={(e) => updateOpt("imgTextMargin", Number(e.target.value) || 0, true)}
            />
          </div>
          <div className="space-y-1">
            <Label className="text-xs text-gray-500">文本内容间距</Label>
            <Input
              type="number"
              min={0}
              step={1}
              value={config.textContentMargin}
              onChange={(e) => updateOpt("textContentMargin", Number(e.target.value) || 0, true)}
            />
          </div>
        </div>
      </div>

      <div className="flex items-center justify-between">
        <Label className="text-xs text-gray-500">AI 功能</Label>
        <Toggle size="sm" pressed={aiOn} onPressedChange={updateAi}>
          {aiOn ? "开" : "关"}
        </Toggle>
      </div>
    </div>
  );
};

const FormulaPanel = ({ mindmap }: { mindmap: any }) => {
  const [formula, setFormula] = useState("a^2+b^2=c^2");
  const insert = () => {
    const active = mindmap?.renderer?.activeNodeList || [];
    mindmap?.execCommand?.("INSERT_FORMULA", formula, active);
  };
  return (
    <div className="space-y-3">
      <Label className="text-xs text-gray-500">LaTeX 公式</Label>
      <Input value={formula} onChange={(e) => setFormula(e.target.value)} />
      <button className="w-full rounded-md bg-blue-500 px-3 py-2 text-sm text-white hover:bg-blue-600" onClick={insert}>
        插入公式
      </button>
    </div>
  );
};

const NotePanel = ({ mindmap, activeNodes }: { mindmap: any; activeNodes: MindMapNode[] }) => {
  const [note, setNote] = useState("");

  const readNote = (node: any) => {
    try {
      return (
        node?.getData?.("note") ??
        node?.nodeData?.data?.note ??
        node?.data?.note ??
        ""
      );
    } catch {
      return "";
    }
  };

  // 当选中节点变化时，自动展示首个节点的备注
  useEffect(() => {
    if (!activeNodes?.length) {
      Promise.resolve().then(() => setNote(""));
      return;
    }
    const current = readNote(activeNodes[0]);
    Promise.resolve().then(() =>
      setNote(typeof current === "string" ? current : ""),
    );
  }, [activeNodes]);

  const getActiveList = () =>
    mindmap?.renderer?.activeNodeList?.length
      ? mindmap.renderer.activeNodeList
      : activeNodes || [];

  const apply = () => {
    const list = getActiveList();
    list.forEach((node: any) => mindmap?.execCommand?.("SET_NODE_NOTE", node, note));
  };

  const clearNote = () => {
    const list = getActiveList();
    list.forEach((node: any) => mindmap?.execCommand?.("SET_NODE_NOTE", node, ""));
    setNote("");
  };

  if (!activeNodes?.length) {
    return <div className="p-4 text-sm text-gray-400">请选择节点后编辑备注</div>;
  }

  return (
    <div className="space-y-3">
      <Label className="text-xs text-gray-500">节点备注（自动载入首个选中节点）</Label>
      <textarea
        className="w-full rounded-md border border-gray-200 p-2 text-sm"
        rows={38}
        value={note}
        onChange={(e) => setNote(e.target.value)}
        placeholder="输入备注内容"
      />
      <div className="flex gap-2">
        <button
          className="flex-1 rounded-md bg-blue-500 px-3 py-2 text-sm text-white hover:bg-blue-600"
          onClick={apply}
        >
          保存 / 更新
        </button>
        <button
          className="flex-1 rounded-md border border-gray-300 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50"
          onClick={clearNote}
        >
          清除备注
        </button>
      </div>
    </div>
  );
};

type AiMode = "chat" | "full" | "partial";

const AiPanel = ({
  mindmap,
  activeNodes,
  documentId,
  mindmapId,
}: {
  mindmap: any;
  activeNodes: MindMapNode[];
  documentId: string;
  mindmapId: string;
}) => {
  const [prompt, setPrompt] = useState("");
  const [mode, setMode] = useState<AiMode>("full");
  const [model, setModel] = useState("qwen3:30b-a3b-instruct-2507-q4_K_M");
  const [baseUrl, setBaseUrl] = useState("http://localhost:11434/api/chat");
  const [systemPrompt, setSystemPrompt] = useState("请用 Markdown，仅使用标题和无序列表结构输出思维导图内容。不要添加额外解释。");
  const [streamText, setStreamText] = useState("");
  const [loading, setLoading] = useState(false);
  const controllerRef = React.useRef<AbortController | null>(null);

  // 文档驱动：从 PDF 大纲生成导图（M1）
  const [docSource, setDocSource] = useState<"test" | "url">("test");
  const [testPdfName, setTestPdfName] = useState("卤化反应原理_1-9.pdf");
  const [docUrl, setDocUrl] = useState("");
  const [docTitle, setDocTitle] = useState("");
  const [docPreferProvider, setDocPreferProvider] = useState<"online" | "ollama" | "heuristic">("online");
  const [docMaxPages, setDocMaxPages] = useState(9);
  const [docLoading, setDocLoading] = useState(false);
  const [docDebug, setDocDebug] = useState("");

  // AI Agent：补完选中节点（服务端：SearxNG + 在线 AI -> ops -> 落盘）
  const [expandInstruction, setExpandInstruction] = useState("");
  const [expandLoading, setExpandLoading] = useState(false);
  const [expandDebug, setExpandDebug] = useState("");
  const [expandUseSearx, setExpandUseSearx] = useState(true);

  const persistMindmapData = (data: unknown): boolean => {
    try {
      const w = window as unknown as {
        __mindmapPersistById?: Record<string, (data: unknown) => void>;
      };
      const fn = w.__mindmapPersistById?.[mindmapId];
      if (typeof fn === "function") {
        fn(data);
        return true;
      }
    } catch {
      // ignore
    }
    return false;
  };

  const resetStream = () => {
    setStreamText("");
  };

  const stop = () => {
    controllerRef.current?.abort();
    controllerRef.current = null;
    setLoading(false);
  };

  const runDocOutlineToMindmap = async () => {
    setDocDebug("");
    setDocLoading(true);
    try {
      const body =
        docSource === "test"
          ? {
              source: { kind: "test", name: testPdfName },
              ollama: { baseUrl, model },
              options: { preferProvider: docPreferProvider, maxPages: docMaxPages },
            }
          : {
              source: { kind: "url", fileUrl: docUrl, title: docTitle || undefined },
              ollama: { baseUrl, model },
              options: { preferProvider: docPreferProvider, maxPages: docMaxPages },
            };

      const res = await fetch("/api/mindmap-ai/outline-to-mindmap", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body),
      });
      const json = (await res.json().catch(() => null)) as any;
      if (!res.ok) {
        throw new Error(String(json?.error ?? `请求失败：${res.status}`));
      }
      if (!json?.mindmapData) {
        throw new Error("接口返回缺少 mindmapData");
      }
      mindmap?.setData?.(json.mindmapData);
      mindmap?.command?.clearHistory?.();
      // 直接用服务端返回的结构化数据保存（避免 setData 异步导致快照仍为旧数据，从而覆盖成空树）
      const ok = persistMindmapData(json.mindmapData);
      if (!ok) {
        // 兜底：延迟触发一次 data_change，让外层自行抓取快照保存
        window.setTimeout(() => {
          try {
            mindmap?.emit?.("data_change");
          } catch {
            // ignore
          }
        }, 250);
      }
      setDocDebug(
        `已生成：${json?.meta?.title ?? "文档"}；provider=${json?.meta?.providerUsed ?? "unknown"}；候选行 ${json?.candidates?.length ?? 0}；节点 ${json?.plan?.chapters ? "plan" : (json?.outline?.length ?? 0)}`,
      );
    } catch (e) {
      setDocDebug(`生成失败：${e instanceof Error ? e.message : String(e)}`);
      window.alert(`从 PDF 生成导图失败：${e instanceof Error ? e.message : String(e)}`);
    } finally {
      setDocLoading(false);
    }
  };

  const runExpandSelectedNode = async () => {
    setExpandDebug("");
    if (!documentId || !mindmapId) {
      window.alert("缺少 documentId/mindmapId，无法补完。");
      return;
    }
    const list = getActiveList();
    if (!list?.length) {
      window.alert("请先选中一个节点再补完。");
      return;
    }
    const node = list[0] as any;
    const uid =
      node?.nodeData?.data?.uid ||
      node?.nodeData?.uid ||
      node?.getData?.("uid") ||
      node?.uid ||
      "";
    if (!uid) {
      window.alert("选中节点缺少 uid，无法补完。");
      return;
    }
    const text =
      node?.getData?.("text") ||
      node?.nodeData?.data?.text ||
      node?.data?.text ||
      "";
    setExpandLoading(true);
    try {
      const res = await fetch("/api/mindmap-ai/expand-node", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          documentId,
          mindmapId,
          targetUid: uid,
          instruction: expandInstruction || undefined,
          sources: { searxng: expandUseSearx },
        }),
      });
      const json = (await res.json().catch(() => null)) as any;
      if (!res.ok) {
        throw new Error(String(json?.error ?? `请求失败：${res.status}`));
      }
      if (!json?.data) {
        throw new Error("接口返回缺少 data");
      }
      mindmap?.setData?.(json.data);
      mindmap?.command?.clearHistory?.();
      // 直接用服务端返回的结构化数据保存（避免 setData 异步导致快照仍为旧数据，从而覆盖成空树）
      const ok = persistMindmapData(json.data);
      if (!ok) {
        // 兜底：延迟触发一次 data_change，让外层自行抓取快照保存
        window.setTimeout(() => {
          try {
            mindmap?.emit?.("data_change");
          } catch {
            // ignore
          }
        }, 250);
      }
      setExpandDebug(
        `已补完：${String(text || "目标节点").slice(0, 50)}；新增 ${json?.applied ?? 0}；searx=${json?.meta?.searched ? "on" : "off"}（${json?.meta?.searxCount ?? 0}）`,
      );
    } catch (e) {
      const msg = e instanceof Error ? e.message : String(e);
      setExpandDebug(`补完失败：${msg}`);
      window.alert(`补完节点失败：${msg}`);
    } finally {
      setExpandLoading(false);
    }
  };

  const runChat = async () => {
    if (!prompt.trim()) return;
    resetStream();
    setLoading(true);
    const controller = new AbortController();
    controllerRef.current = controller;
    try {
      const res = await fetch(baseUrl, {
        method: "POST",
        signal: controller.signal,
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          model,
          stream: true,
          messages: [
            { role: "system", content: systemPrompt },
            { role: "user", content: prompt.trim() },
          ],
        }),
      });
      const reader = res.body?.getReader();
      if (!reader) throw new Error("无法读取流");
      const decoder = new TextDecoder();
      let full = "";
      while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        const chunk = decoder.decode(value);
        // Ollama 返回 json 行
        const lines = chunk.split("\n").filter(Boolean);
        for (const line of lines) {
          try {
            const json = JSON.parse(line);
            if (json.message?.content) {
              full += json.message.content;
              setStreamText((prev) => prev + json.message.content);
            }
          } catch {
            // 忽略非 json 行
          }
        }
      }
      return full;
    } finally {
      setLoading(false);
      controllerRef.current = null;
    }
  };

  // 将 AI 文本解析为子节点数组，尽量兼容宽松 Markdown 列表
  const parseChildrenFromText = async (text: string) => {
    const { transformMarkdownTo } = await import("simple-mind-map/src/parse/markdownTo.js");
    const { createUid } = await import("simple-mind-map/src/utils/index.js");
    const safeText = text || "";
    let children = transformMarkdownTo(safeText)?.children || [];
    if (!children.length) {
      const lines = safeText
        .split(/\r?\n/)
        .map((l) => l.trim())
        .filter(Boolean);
      const listLines = lines.filter((l) => /^[-*+]\s+/.test(l)).map((l) => l.replace(/^[-*+]\s+/, ""));
      const headingLines = lines.filter((l) => /^#+\s+/.test(l)).map((l) => l.replace(/^#+\s+/, ""));
      const useLines = listLines.length ? listLines : headingLines.length ? headingLines : lines;
      children = useLines.map((t) => ({ data: { text: t } }));
    }
    if (!children.length) {
      children = [{ data: { text: safeText.slice(0, 200) || "新节点" } }];
    }

    const fill = (nodes: any[]) => {
      nodes.forEach((node) => {
        if (!node.data) node.data = {};
        if (!node.data.text) node.data.text = "新节点";
        if (!node.data.uid) node.data.uid = createUid();
        if (node.children?.length) fill(node.children);
      });
    };
    fill(children);
    return JSON.parse(JSON.stringify(children));
  };

  const getActiveList = () =>
    (activeNodes && activeNodes.length ? activeNodes : mindmap?.renderer?.activeNodeList || []) as any[];

  const updateDataByUids = async (uids: string[], updater: (node: any) => void) => {
    const source = mindmap?.getData?.(true) || mindmap?.getData?.();
    const dataClone = source ? JSON.parse(JSON.stringify(source)) : null;
    if (!dataClone) {
      window.alert("无法获取导图数据，操作失败。");
      return false;
    }

    // 确保整棵树都有 uid，避免官方逻辑依赖 uid 时抛错
    const { createUid } = await import("simple-mind-map/src/utils/index.js");
    const sanitizeNode = (node: any) => {
      if (!node || typeof node !== "object") return null;
      if (!node.data) node.data = {};
      if (!node.data.uid) node.data.uid = createUid();
      if (typeof node.data.text !== "string") {
        node.data.text =
          node.data.text != null ? String(node.data.text) : "新节点";
      }
      if (Array.isArray(node.children)) {
        node.children = node.children
          .map((c: any) => sanitizeNode(c))
          .filter(Boolean);
      } else {
        node.children = [];
      }
      return node;
    };
    sanitizeNode(dataClone);

    const cleanRenderCallbacks = () => {
      try {
        const r = mindmap?.renderer;
        if (r && Array.isArray((r as any).renderCallbackList)) {
          r.renderCallbackList = (r.renderCallbackList as any[]).filter(
            (fn) => typeof fn === "function",
          );
        }
      } catch {
        // ignore
      }
    };
    cleanRenderCallbacks();

    const walk = (node: any) => {
      if (uids.includes(node?.data?.uid)) {
        updater(node);
      }
      if (node.children?.length) {
        node.children.forEach(walk);
      }
    };
    walk(dataClone);
    cleanRenderCallbacks();
    mindmap?.updateData?.(dataClone);
    cleanRenderCallbacks();
    return true;
  };

  const applyFullMindmap = async (content: string) => {
    if (!content.trim()) return;
    try {
      const { transformMarkdownTo } = await import("simple-mind-map/src/parse/markdownTo.js");
      const data = transformMarkdownTo(content);
      if (!data?.data) {
        data.data = { text: "中心主题" };
      }
      mindmap?.setData?.(data);
      mindmap?.command?.clearHistory?.();
    } catch (error) {
      console.error(error);
      window.alert("解析 AI 结果失败，请检查格式（需 Markdown 标题和无序列表）。");
    }
  };

  const applyPartialMindmap = async (text: string, presetChildren?: any[]) => {
    const list = getActiveList();
    if (!list?.length) {
      window.alert("请选择节点后再续写。");
      return;
    }
    try {
      const children = presetChildren || (await parseChildrenFromText(text));
      const uids = list
        .map(
          (node: any) =>
            node?.nodeData?.data?.uid ||
            node?.nodeData?.uid ||
            node?.getData?.("uid") ||
            node?.uid,
        )
        .filter(Boolean);
      if (!uids.length) {
        window.alert("未获取到选中节点的 UID，续写失败。");
        return;
      }
      const ok = await updateDataByUids(uids as string[], (node) => {
        node.children = Array.isArray(node.children) ? node.children : [];
        const childrenCopy = JSON.parse(JSON.stringify(children));
        node.children.push(...childrenCopy);
      });
      if (!ok) return;
      mindmap?.renderer?.render?.(mindmap?.getData?.());
    } catch (e) {
      console.error("AI Markdown Parsing Error:", e);
      if (e instanceof Error) {
          console.error("Error Message:", e.message);
          console.error("Error Stack:", e.stack);
      }
      window.alert("解析 AI 返回的 Markdown 失败，请检查格式。");
    }
  };

  const handleSend = async () => {
    const text = (await runChat()) || streamText;
    if (mode === "full" && text) {
      await applyFullMindmap(text);
    }
    if (mode === "partial" && text) {
      await applyPartialMindmap(text);
    }
  };

  const handleSaveToNote = () => {
    const text = streamText.trim();
    if (!text) {
      window.alert("暂无可保存的内容");
      return;
    }
    const list = getActiveList();
    if (!list?.length) {
      window.alert("请选择节点后再保存备注");
      return;
    }
    list.forEach((node: any) => mindmap?.execCommand?.("SET_NODE_NOTE", node, text));
  };

  const handleAppendSingleChild = async () => {
    const text = streamText.trim();
    if (!text) {
      window.alert("暂无可生成的内容");
      return;
    }
    const children = await parseChildrenFromText(text.slice(0, 500));
    const first = children.length ? [children[0]] : [{ data: { text } }];
    await applyPartialMindmap("", first);
  };

  const handleAppendMultiChild = async () => {
    const text = streamText.trim();
    if (!text) {
      window.alert("暂无可生成的内容");
      return;
    }
    const children = await parseChildrenFromText(text);
    await applyPartialMindmap("", children);
  };

  const handleReplaceCurrent = async () => {
    const text = streamText.trim();
    if (!text) {
      window.alert("暂无可替换的内容");
      return;
    }
    const list = getActiveList();
    if (!list?.length) {
      window.alert("请选择节点后再修改");
      return;
    }
    const uids = list
      .map(
        (node: any) =>
          node?.nodeData?.data?.uid ||
          node?.nodeData?.uid ||
          node?.getData?.("uid") ||
          node?.uid,
      )
      .filter(Boolean) as string[];
    const ok = await updateDataByUids(uids, (node) => {
      if (!node.data) node.data = {};
      node.data.text = text;
    });
    if (ok) {
      mindmap?.renderer?.render?.(mindmap?.getData?.());
    }
  };

  return (
    <div className="space-y-3">
      <div className="space-y-2 rounded-md border border-gray-200 p-3">
        <div className="flex items-center justify-between">
          <Label className="text-xs text-gray-500">文档导图（按大纲生成）</Label>
          {docSource === "test" && (
            <a
              className="text-xs text-blue-600 hover:underline"
              href={`/api/mindmap-ai/test-pdf?name=${encodeURIComponent(testPdfName)}`}
              target="_blank"
              rel="noreferrer"
            >
              打开 PDF
            </a>
          )}
        </div>

        <div className="grid grid-cols-2 gap-2">
          <button
            type="button"
            className={`rounded-md border px-2 py-2 text-sm hover:bg-gray-50 ${docSource === "test" ? "border-blue-500 text-blue-600" : "border-gray-300 text-gray-700"}`}
            onClick={() => setDocSource("test")}
          >
            测试 PDF
          </button>
          <button
            type="button"
            className={`rounded-md border px-2 py-2 text-sm hover:bg-gray-50 ${docSource === "url" ? "border-blue-500 text-blue-600" : "border-gray-300 text-gray-700"}`}
            onClick={() => setDocSource("url")}
          >
            URL / Signed URL
          </button>
        </div>

        {docSource === "test" ? (
          <div className="space-y-1">
            <Label className="text-xs text-gray-500">测试文件名（wolai-frontend/test）</Label>
            <Input
              value={testPdfName}
              onChange={(e) => setTestPdfName(e.target.value)}
              placeholder="例如：卤化反应原理_1-9.pdf"
            />
            <p className="text-xs text-gray-400">仅本地开发可用：用于快速验证“生成导图 + 节点跳页链接”。</p>
          </div>
        ) : (
          <div className="space-y-2">
            <div className="space-y-1">
              <Label className="text-xs text-gray-500">PDF 地址</Label>
              <Input
                value={docUrl}
                onChange={(e) => setDocUrl(e.target.value)}
                placeholder="http://127.0.0.1:xxx/file.pdf 或 supabase signed url"
              />
            </div>
            <div className="space-y-1">
              <Label className="text-xs text-gray-500">标题（可选）</Label>
              <Input value={docTitle} onChange={(e) => setDocTitle(e.target.value)} placeholder="不填则使用“文档”" />
            </div>
            <p className="text-xs text-gray-400">出于安全考虑，当前仅允许本机或 supabase 域名。</p>
          </div>
        )}

        <div className="grid grid-cols-2 gap-2">
          <div className="space-y-1">
            <Label className="text-xs text-gray-500">优先模型</Label>
            <NativeSelect
              value={docPreferProvider}
              onChange={(v) => setDocPreferProvider(v as "online" | "ollama" | "heuristic")}
              options={[
                { label: "在线 AI（默认）", value: "online" },
                { label: "本地 Ollama", value: "ollama" },
                { label: "规则兜底", value: "heuristic" },
              ]}
            />
          </div>
          <div className="space-y-1">
            <Label className="text-xs text-gray-500">最大页数</Label>
            <Input
              type="number"
              min={1}
              max={200}
              step={1}
              value={docMaxPages}
              onChange={(e) => setDocMaxPages(Number(e.target.value) || 1)}
            />
          </div>
        </div>

        <button
          type="button"
          disabled={docLoading || (docSource === "url" && !docUrl.trim())}
          className="w-full rounded-md bg-blue-500 px-3 py-2 text-sm text-white hover:bg-blue-600 disabled:opacity-50"
          onClick={runDocOutlineToMindmap}
        >
          {docLoading ? "生成中..." : "从 PDF 生成导图（替换当前）"}
        </button>
        {docDebug && <div className="text-xs text-gray-500 whitespace-pre-wrap">{docDebug}</div>}
      </div>

      {MINDMAP_LEGACY_EXPAND_NODE_ENTRY_ENABLED ? (
        <div className="space-y-2 rounded-md border border-gray-200 p-3">
          <div className="flex items-center justify-between">
            <Label className="text-xs text-gray-500">AI 补完（选中节点）</Label>
            <div className="flex items-center gap-2">
              <Toggle
                pressed={expandUseSearx}
                onPressedChange={(v) => setExpandUseSearx(Boolean(v))}
                size="sm"
                className="text-xs"
              >
                <Network className="h-4 w-4 mr-1" />
                联网
              </Toggle>
            </div>
          </div>
          <textarea
            className="w-full rounded-md border border-gray-200 p-2 text-sm"
            rows={3}
            value={expandInstruction}
            onChange={(e) => setExpandInstruction(e.target.value)}
            placeholder="例如：补充该节点的关键概念、常见误区与参考链接（每条都要可点击来源）"
          />
          <button
            type="button"
            disabled={expandLoading}
            className="w-full rounded-md bg-blue-500 px-3 py-2 text-sm text-white hover:bg-blue-600 disabled:opacity-50"
            onClick={runExpandSelectedNode}
          >
            {expandLoading ? "补完中..." : "补完选中节点（写入并保存）"}
          </button>
          {expandDebug && <div className="text-xs text-gray-500 whitespace-pre-wrap">{expandDebug}</div>}
          <p className="text-xs text-gray-400">
            说明：服务端会用 SearxNG 检索证据 + 在线 AI 生成 ops，并自动落盘到当前 mindmap 文件中。
          </p>
        </div>
      ) : null}

      <Label className="text-xs text-gray-500">模式</Label>
      <NativeSelect
        value={mode}
        onChange={(v) => setMode(v as AiMode)}
        options={[
          { label: "整图生成", value: "full" },
          { label: "对话", value: "chat" },
          { label: "选中节点续写", value: "partial" },
        ]}
      />
      <div className="space-y-2">
        <Label className="text-xs text-gray-500">模型</Label>
        <Input value={model} onChange={(e) => setModel(e.target.value)} />
      </div>
      <div className="space-y-2">
        <Label className="text-xs text-gray-500">接口地址</Label>
        <Input value={baseUrl} onChange={(e) => setBaseUrl(e.target.value)} />
      </div>
      <div className="space-y-2">
        <Label className="text-xs text-gray-500">系统提示</Label>
        <Input value={systemPrompt} onChange={(e) => setSystemPrompt(e.target.value)} />
      </div>
      <div className="space-y-2">
        <Label className="text-xs text-gray-500">{mode === "full" ? "生成主题" : mode === "partial" ? "续写内容" : "对话输入"}</Label>
        <textarea
          className="w-full rounded-md border border-gray-200 p-2 text-sm"
          rows={5}
          value={prompt}
          onChange={(e) => setPrompt(e.target.value)}
          placeholder={
            mode === "full"
              ? "例如：帮我生成一份 OKR 思维导图"
              : mode === "partial"
                ? "描述要续写的方向或要补充的要点"
                : "输入你想询问的内容"
          }
        />
      </div>
      <div className="flex gap-2">
        <button
          disabled={loading}
          className="flex-1 rounded-md bg-blue-500 px-3 py-2 text-sm text-white hover:bg-blue-600 disabled:opacity-50"
          onClick={handleSend}
        >
          {loading ? "生成中..." : "发送"}
        </button>
        <button
          disabled={!loading}
          className="w-20 rounded-md border border-gray-300 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 disabled:opacity-50"
          onClick={stop}
        >
          停止
        </button>
      </div>
      <div className="space-y-1">
        <Label className="text-xs text-gray-500">流式输出</Label>
        <div className="h-40 overflow-auto rounded-md border border-gray-200 p-2 text-sm whitespace-pre-wrap">
          {streamText || "等待输出..."}
        </div>
      </div>
      <div className="space-y-1">
        <Label className="text-xs text-gray-500">输出处理</Label>
        <div className="grid grid-cols-2 gap-2">
          <button
            className="rounded-md border border-gray-300 px-2 py-2 text-sm hover:bg-gray-50"
            onClick={handleSaveToNote}
          >
            生成到备注
          </button>
          <button
            className="rounded-md border border-gray-300 px-2 py-2 text-sm hover:bg-gray-50"
            onClick={handleAppendSingleChild}
          >
            生成为单一子节点
          </button>
          <button
            className="rounded-md border border-gray-300 px-2 py-2 text-sm hover:bg-gray-50"
            onClick={handleAppendMultiChild}
          >
            生成为多个子节点
          </button>
          <button
            className="rounded-md border border-gray-300 px-2 py-2 text-sm hover:bg-gray-50"
            onClick={handleReplaceCurrent}
          >
            修改当前节点
          </button>
        </div>
      </div>
      {mode === "full" && (
        <p className="text-xs text-gray-400">
          将使用 AI 生成完整思维导图，返回的 Markdown 会自动解析并替换当前导图。请确保本地 Ollama 已启动且模型已下载。
        </p>
      )}
      {mode === "partial" && (
        <p className="text-xs text-gray-400">
          续写模式：请选择一个或多个节点，AI 返回的 Markdown 子列表会追加为所选节点的子节点。
        </p>
      )}
    </div>
  );
};

export const MindmapSidebar = ({
  documentId,
  mindmapId,
  mindmap,
  activeNodes,
  activeTab,
  onClose,
}: SidebarProps) => {
  const content = useMemo(() => {
    switch (activeTab as SidebarPanel | null) {
      case "style":
        return <StylePanel activeNodes={activeNodes} />;
      case "base":
        return <BaseStylePanel mindmap={mindmap} />;
      case "theme":
        return <ThemePanel mindmap={mindmap} />;
      case "structure":
        return <StructurePanel mindmap={mindmap} />;
      case "icons":
        return <IconPanel activeNodes={activeNodes} />;
      case "outline":
        return <OutlinePanel mindmap={mindmap} />;
      case "settings":
        return <SettingsPanel mindmap={mindmap} />;
      case "formula":
        return <FormulaPanel mindmap={mindmap} />;
      case "note":
        return <NotePanel mindmap={mindmap} activeNodes={activeNodes} />;
      case "ai":
        return (
          <MindmapAiAgentPanel
            mindmap={mindmap}
            activeNodes={activeNodes}
            documentId={documentId}
            mindmapId={mindmapId}
            onClose={onClose}
          />
        );
      default:
        return null;
    }
  }, [activeTab, mindmap, activeNodes, documentId, mindmapId, onClose]);

  const title = useMemo(() => {
    if (!activeTab) return "";
    return sidebarTitles[activeTab as SidebarPanel] ?? "";
  }, [activeTab]);

  return (
    <div
      className={`absolute top-0 bottom-0 right-0 z-20 w-[300px] flex flex-col border-l border-gray-200 bg-white transition-transform duration-300 ease-in-out shadow-lg ${
        activeTab ? "translate-x-0" : "translate-x-full"
      }`}
    >
      {activeTab === "ai" ? null : (
        <div className="flex items-center justify-between border-b px-4 py-3 shrink-0">
          <span className="font-medium text-gray-700">{title}</span>
          <button onClick={onClose} className="text-gray-400 hover:text-gray-600">
            <X className="h-4 w-4" />
          </button>
        </div>
      )}
      <div className={activeTab === "ai" ? "flex-1 overflow-hidden p-0" : "flex-1 overflow-y-auto px-4 py-4"}>
        {content}
      </div>
    </div>
  );
};
