"use client";

import {
  addEdge,
  Background,
  BackgroundVariant,
  type Connection,
  Controls,
  type Edge,
  type Node,
  type NodeChange,
  type OnConnectEnd,
  Panel,
  ReactFlow,
  useEdgesState,
  useNodesState,
  useReactFlow,
} from "@xyflow/react";
import { Braces, Plus, Settings2 } from "lucide-react";
import {
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from "react";

import { EditableTitle } from "@/components/assistant-editor/editor-controls";
import { Button } from "@/components/ui/button";
import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from "@/components/ui/tooltip";

import { AddNodeDialog } from "./AddNodeDialog";
import { edgeTypes } from "./ConditionEdge";
import { hasDefaultPath, newEdgeData } from "./edge-rules";
import {
  ActiveNodeContext,
  EdgeActionContext,
  NodeActionContext,
  NodeSpecsContext,
} from "./context";
import { nodeTypes } from "./GenericNode";
import { EdgeSettingsPanel } from "./panels/EdgeSettingsPanel";
import { GlobalSettingsPanel } from "./panels/GlobalSettingsPanel";
import { NodeSettingsPanel } from "./panels/NodeSettingsPanel";
import { WorkflowSidePanel } from "./WorkflowSidePanel";
import {
  defaultGraph,
  type NodeSpecMap,
  type RuntimeNodeSpec,
  type WorkflowGraph,
  type WorkflowEdgeData,
  type WorkflowNodeData,
  type WorkflowNodeType,
} from "./specs";
import type { WorkflowEditorProps } from "./types";
import { workflowGraphWithSettings } from "./graph-settings";

let nodeSeq = 0;

function defaultNodeData(spec: RuntimeNodeSpec): WorkflowNodeData {
  const data: WorkflowNodeData = {
    name: spec.displayName,
  };
  if (spec.type === "agent") {
    Object.assign(data, {
      contextPolicy: "inherit",
      inheritGlobalConfig: true,
      entryMode: "wait_user",
    });
  } else if (spec.type === "action") {
    Object.assign(data, {
      arguments: {},
      resultAssignmentMode: "inherit",
      userInputPolicy: "queue",
    });
  } else if (spec.type === "message") {
    Object.assign(data, {
      speech: "",
      title: "重要提示",
      message: "",
      confirmLabel: "确认",
      completionPolicy: "playback",
    });
  }
  for (const field of spec.fields) {
    if (field.default !== undefined) data[field.key] = field.default;
  }
  return data;
}

function toFlow(graph: WorkflowGraph): { nodes: Node[]; edges: Edge[] } {
  return {
    nodes: graph.nodes.map((n) => ({
      id: n.id,
      type: n.type,
      position: n.position,
      data: n.data,
    })),
    edges: graph.edges.map((e) => ({
      id: e.id,
      type: "condition",
      source: e.source,
      target: e.target,
      data: e.data ?? {},
    })),
  };
}

function fromFlow(nodes: Node[], edges: Edge[]): WorkflowGraph {
  return {
    specVersion: 3,
    settings: {
      globalPrompt: "",
      defaultLlmResourceId: "",
      defaultAsrResourceId: "",
      defaultTtsResourceId: "",
      visionEnabled: false,
      visionModelResourceId: "",
      toolIds: [],
      knowledgeBaseId: "",
      knowledgeMode: "automatic",
      knowledgeTopN: 5,
      knowledgeScoreThreshold: 0,
      enableInterrupt: true,
      turnConfig: defaultGraph().settings.turnConfig,
    },
    nodes: nodes.map((n) => ({
      id: n.id,
      type: n.type as WorkflowNodeType,
      position: n.position,
      data: n.data as WorkflowNodeData,
    })),
    edges: edges.map((e) => ({
      id: e.id,
      source: e.source,
      target: e.target,
      data: (e.data ?? {
        mode: "always",
        priority: 10,
      }) as WorkflowGraph["edges"][number]["data"],
    })),
  };
}

export function WorkflowCanvas({
  value,
  onChange,
  settings,
  onSettingsChange,
  modelOptions,
  activeNodeId,
  dynamicVariablesOpen,
  onDynamicVariablesOpenChange,
  dynamicVariablesPanel,
  editingNodeId,
  onEditingNodeIdChange,
  editingEdgeId,
  onEditingEdgeIdChange,
  settingsOpen,
  onSettingsOpenChange: setSettingsOpen,
  debugOpen,
  onDebugOpenChange,
  debugPanel,
  specsByType,
  toolOptions = [],
  knowledgeOptions = [],
}: WorkflowEditorProps & { specsByType: NodeSpecMap }) {
  const initial = useMemo(() => toFlow(value ?? defaultGraph()), [value]);
  const [nodes, setNodes, onNodesChange] = useNodesState(initial.nodes);
  const [edges, setEdges, onEdgesChange] = useEdgesState(initial.edges);
  const [addOpen, setAddOpen] = useState(false);
  const [addSourceId, setAddSourceId] = useState<string | null>(null);
  const [addPosition, setAddPosition] = useState<{ x: number; y: number } | null>(null);
  // Fast Refresh 会替换模块级对象，而保留 React Flow 实例。用 state 固定本次
  // 挂载使用的映射，避免热更新后被误判为每次渲染都在创建新对象。
  const [flowNodeTypes] = useState(nodeTypes);
  const [flowEdgeTypes] = useState(edgeTypes);
  const { screenToFlowPosition } = useReactFlow();

  // 只在可持久化的画布内容真正变化时回传。React Flow 挂载时会写入节点尺寸等
  // 内部状态，这些不属于 workflow graph，不能因此把刚加载的助手标记为未保存。
  const onChangeRef = useRef(onChange);
  const lastSyncedGraphRef = useRef(
    JSON.stringify(
      workflowGraphWithSettings(
        fromFlow(initial.nodes, initial.edges),
        settings,
      ),
    ),
  );
  useEffect(() => {
    onChangeRef.current = onChange;
  }, [onChange]);
  useEffect(() => {
    const graph = workflowGraphWithSettings(fromFlow(nodes, edges), settings);
    const nextGraphJson = JSON.stringify(graph);
    if (nextGraphJson === lastSyncedGraphRef.current) return;

    lastSyncedGraphRef.current = nextGraphJson;
    onChangeRef.current?.(graph);
  }, [nodes, edges, settings]);

  const onConnect = useCallback(
    (connection: Connection) => {
      if (!connection.source || !connection.target) return;
      const sourceType = nodes.find(
        (node) => node.id === connection.source,
      )?.type;
      setEdges((eds) =>
        addEdge(
          {
            ...connection,
            id: `e-${connection.source}-${connection.target}-${Date.now()}`,
            type: "condition",
            animated: true,
            data: newEdgeData(eds, connection.source, sourceType),
          },
          eds,
        ),
      );
    },
    [nodes, setEdges],
  );

  // 连线约束:不能连入开始节点(无入边句柄),不能自连。
  const isValidConnection = useCallback(
    (c: Connection | Edge) => {
      if (c.source === c.target) return false;
      const source = nodes.find((n) => n.id === c.source);
      const target = nodes.find((n) => n.id === c.target);
      if (!source || !target) return false;

      const sourceSpec = specsByType[source.type as string];
      const targetSpec = specsByType[target.type as string];
      if (!sourceSpec?.hasSource || !targetSpec?.hasTarget) return false;
      if (edges.some((e) => e.source === c.source && e.target === c.target)) {
        return false;
      }

      const sourceLimit = sourceSpec.constraints.maxOutgoing;
      if (
        sourceLimit !== undefined &&
        edges.filter((e) => e.source === c.source).length >= sourceLimit
      ) {
        return false;
      }
      const targetLimit = targetSpec.constraints.maxIncoming;
      if (
        targetLimit !== undefined &&
        edges.filter((e) => e.target === c.target).length >= targetLimit
      ) {
        return false;
      }
      return true;
    },
    [edges, nodes, specsByType],
  );

  const addNode = useCallback(
    (spec: RuntimeNodeSpec) => {
      nodeSeq += 1;
      const id = `${spec.type}-${Date.now()}-${nodeSeq}`;
      const source = addSourceId
        ? nodes.find((node) => node.id === addSourceId)
        : undefined;
      const position = addPosition
        ? { x: addPosition.x - 125, y: addPosition.y }
        : screenToFlowPosition({
            x: window.innerWidth / 2,
            y: window.innerHeight / 2,
          });
      const data = defaultNodeData(spec);
      setNodes((ns) => [...ns, { id, type: spec.type, position, data }]);
      if (source) {
        setEdges((currentEdges) => {
          return addEdge(
            {
              id: `e-${source.id}-${id}-${Date.now()}`,
              source: source.id,
              target: id,
              type: "condition",
              animated: true,
              data: newEdgeData(currentEdges, source.id, source.type),
            },
            currentEdges,
          );
        });
      }
      setAddOpen(false);
      setAddSourceId(null);
      setAddPosition(null);
      setSettingsOpen(false);
      onDynamicVariablesOpenChange(false);
      onDebugOpenChange(false);
      onEditingEdgeIdChange(null);
      onEditingNodeIdChange(id);
    },
    [
      addPosition,
      addSourceId,
      nodes,
      onDebugOpenChange,
      onDynamicVariablesOpenChange,
      onEditingEdgeIdChange,
      onEditingNodeIdChange,
      screenToFlowPosition,
      setEdges,
      setNodes,
      setSettingsOpen,
    ],
  );

  const updateNodeData = useCallback(
    (id: string, patch: Partial<WorkflowNodeData>) => {
      setNodes((ns) =>
        ns.map((n) =>
          n.id === id ? { ...n, data: { ...n.data, ...patch } } : n,
        ),
      );
    },
    [setNodes],
  );

  const deleteNode = useCallback(
    (id: string) => {
      if (nodes.find((node) => node.id === id)?.type === "start") return;
      setNodes((ns) => ns.filter((n) => n.id !== id));
      setEdges((es) => es.filter((e) => e.source !== id && e.target !== id));
      if (editingNodeId === id) onEditingNodeIdChange(null);
    },
    [editingNodeId, nodes, onEditingNodeIdChange, setNodes, setEdges],
  );

  const duplicateNode = useCallback(
    (id: string) => {
      const source = nodes.find((node) => node.id === id);
      if (!source || source.type === "start") return;

      const spec = specsByType[source.type as string];
      if (!spec) return;
      const limit = spec.constraints.maxInstances;
      if (
        limit !== undefined &&
        nodes.filter((node) => node.type === source.type).length >= limit
      ) {
        return;
      }

      nodeSeq += 1;
      const duplicateId = `${source.type}-${Date.now()}-${nodeSeq}`;
      const sourceData = source.data as WorkflowNodeData;
      const duplicateData = structuredClone(sourceData);
      duplicateData.name = `${sourceData.name || spec.displayName}（副本）`;

      setNodes((currentNodes) => [
        ...currentNodes.map((node) => ({ ...node, selected: false })),
        {
          id: duplicateId,
          type: source.type,
          position: {
            x: source.position.x + 40,
            y: source.position.y + 40,
          },
          data: duplicateData,
          selected: true,
        },
      ]);
      onEditingNodeIdChange(null);
    },
    [
      nodes,
      onEditingNodeIdChange,
      setNodes,
      specsByType,
    ],
  );

  const handleNodesChange = useCallback(
    (changes: NodeChange[]) => {
      const startIds = new Set(
        nodes.filter((node) => node.type === "start").map((node) => node.id),
      );
      onNodesChange(
        changes.filter(
          (change) => change.type !== "remove" || !startIds.has(change.id),
        ),
      );
    },
    [nodes, onNodesChange],
  );

  const updateEdgeData = useCallback(
    (
      id: string,
      patch: Partial<WorkflowEdgeData>,
    ) => {
      setEdges((es) =>
        es.map((e) => {
          if (e.id !== id) return e;
          if (
            patch.mode === "always" &&
            hasDefaultPath(es, e.source, e.id)
          ) {
            return e;
          }
          return { ...e, data: { ...(e.data ?? {}), ...patch } };
        }),
      );
    },
    [setEdges],
  );

  const deleteEdge = useCallback(
    (id: string) => {
      setEdges((es) => es.filter((e) => e.id !== id));
      if (editingEdgeId === id) onEditingEdgeIdChange(null);
    },
    [editingEdgeId, onEditingEdgeIdChange, setEdges],
  );

  const canCreateFromSource = useCallback(
    (id: string) => {
      const source = nodes.find((node) => node.id === id);
      if (!source) return false;
      const spec = specsByType[source.type as string];
      if (!spec?.hasSource) return false;
      const outgoingCount = edges.filter((edge) => edge.source === id).length;
      if (
        spec.constraints.maxOutgoing !== undefined &&
        outgoingCount >= spec.constraints.maxOutgoing
      ) {
        return false;
      }
      return true;
    },
    [edges, nodes, specsByType],
  );
  const onConnectEnd = useCallback<OnConnectEnd>(
    (event, connectionState) => {
      if (
        connectionState.isValid ||
        connectionState.toNode ||
        connectionState.fromHandle?.type !== "source" ||
        !connectionState.fromNode ||
        !canCreateFromSource(connectionState.fromNode.id)
      ) {
        return;
      }
      const pointer = "changedTouches" in event
        ? event.changedTouches[0]
        : event;
      if (!pointer) return;
      setAddSourceId(connectionState.fromNode.id);
      setAddPosition(
        screenToFlowPosition({ x: pointer.clientX, y: pointer.clientY }),
      );
      setAddOpen(true);
    },
    [canCreateFromSource, screenToFlowPosition],
  );
  const openSettings = useCallback(() => {
    onDebugOpenChange(false);
    onDynamicVariablesOpenChange(false);
    onEditingNodeIdChange(null);
    onEditingEdgeIdChange(null);
    setSettingsOpen(true);
  }, [
    onDebugOpenChange,
    onDynamicVariablesOpenChange,
    onEditingEdgeIdChange,
    onEditingNodeIdChange,
    setSettingsOpen,
  ]);

  const nodeActions = useMemo(
    () => ({
      edit: (id: string) => {
        setSettingsOpen(false);
        onDynamicVariablesOpenChange(false);
        onDebugOpenChange(false);
        onEditingEdgeIdChange(null);
        onEditingNodeIdChange(id);
      },
      duplicate: duplicateNode,
      remove: deleteNode,
    }),
    [
      deleteNode,
      duplicateNode,
      onDebugOpenChange,
      onDynamicVariablesOpenChange,
      onEditingEdgeIdChange,
      onEditingNodeIdChange,
      setSettingsOpen,
    ],
  );
  const edgeActions = useMemo(
    () => ({
      edit: (id: string) => {
        setSettingsOpen(false);
        onDynamicVariablesOpenChange(false);
        onDebugOpenChange(false);
        onEditingNodeIdChange(null);
        onEditingEdgeIdChange(id);
      },
      remove: deleteEdge,
    }),
    [
      deleteEdge,
      onDebugOpenChange,
      onDynamicVariablesOpenChange,
      onEditingEdgeIdChange,
      onEditingNodeIdChange,
      setSettingsOpen,
    ],
  );
  const openDynamicVariables = useCallback(() => {
    setSettingsOpen(false);
    onDebugOpenChange(false);
    onEditingNodeIdChange(null);
    onEditingEdgeIdChange(null);
    onDynamicVariablesOpenChange(true);
  }, [
    onDebugOpenChange,
    onDynamicVariablesOpenChange,
    onEditingEdgeIdChange,
    onEditingNodeIdChange,
    setSettingsOpen,
  ]);

  const editingNode = nodes.find((n) => n.id === editingNodeId);
  const editingSpec = editingNode ? specsByType[editingNode.type as string] : null;
  const editingEdge = edges.find((e) => e.id === editingEdgeId);
  const editingEdgeData = editingEdge?.data as WorkflowEdgeData | undefined;
  const addableSpecs = Object.values(specsByType).filter((s) => s.addable);
  const canAddSpec = useCallback(
    (spec: RuntimeNodeSpec) => {
      const limit = spec.constraints.maxInstances;
      if (limit === undefined) return true;
      return nodes.filter((node) => node.type === spec.type).length < limit;
    },
    [nodes],
  );

  return (
    <NodeSpecsContext.Provider value={specsByType}>
      <ActiveNodeContext.Provider value={activeNodeId ?? null}>
      <NodeActionContext.Provider value={nodeActions}>
        <EdgeActionContext.Provider value={edgeActions}>
          <div className="relative h-full w-full min-h-[560px]">
            <section className="relative h-full w-full overflow-hidden rounded-2xl border border-hairline bg-canvas-soft shadow-sm">
              <div
                aria-hidden
                className="pointer-events-none absolute -right-24 -top-24 z-0 h-80 w-80 rounded-full opacity-30 blur-3xl"
                style={{
                  background:
                    "radial-gradient(circle, var(--gradient-sky), transparent 68%)",
                }}
              />
              <div
                aria-hidden
                className="pointer-events-none absolute -bottom-28 left-1/4 z-0 h-72 w-72 rounded-full opacity-25 blur-3xl"
                style={{
                  background:
                    "radial-gradient(circle, var(--gradient-lavender), transparent 68%)",
                }}
              />

              <ReactFlow
                nodes={nodes}
                edges={edges}
                nodeTypes={flowNodeTypes}
                edgeTypes={flowEdgeTypes}
                onNodesChange={handleNodesChange}
                onEdgesChange={onEdgesChange}
                onConnect={onConnect}
                onConnectEnd={onConnectEnd}
                onNodeClick={(_, node) => nodeActions.edit(node.id)}
                onEdgeClick={(_, edge) => edgeActions.edit(edge.id)}
                isValidConnection={isValidConnection}
                onPaneClick={() => {
                  onEditingEdgeIdChange(null);
                }}
                fitView
                proOptions={{ hideAttribution: true }}
                defaultEdgeOptions={{ type: "condition", animated: true }}
              >
                <Background
                  variant={BackgroundVariant.Dots}
                  gap={22}
                  size={1}
                  color="var(--hairline-strong)"
                />
                <Controls
                  className="!rounded-xl !border !border-hairline !bg-card !shadow-sm [&_button]:!border-hairline [&_button]:!bg-card [&_button]:!text-foreground"
                />
                <Panel position="top-left">
                  <TooltipProvider>
                    <div className="flex flex-col gap-2">
                      <Tooltip>
                        <TooltipTrigger asChild>
                          <Button
                            size="icon"
                            className="h-10 w-10 rounded-full shadow-sm"
                            aria-label="添加节点"
                            onClick={() => {
                              onDynamicVariablesOpenChange(false);
                              setAddSourceId(null);
                              setAddPosition(null);
                              setAddOpen(true);
                            }}
                          >
                            <Plus size={17} />
                          </Button>
                        </TooltipTrigger>
                        <TooltipContent side="right">添加节点</TooltipContent>
                      </Tooltip>

                      <Tooltip>
                        <TooltipTrigger asChild>
                          <Button
                            size="icon"
                            variant="outline"
                            className="h-10 w-10 rounded-full border-hairline-strong bg-card text-foreground shadow-sm hover:bg-surface-strong"
                            aria-label="工作流设置"
                            onClick={openSettings}
                          >
                            <Settings2 size={17} />
                          </Button>
                        </TooltipTrigger>
                        <TooltipContent side="right">工作流设置</TooltipContent>
                      </Tooltip>

                      <Tooltip>
                        <TooltipTrigger asChild>
                          <Button
                            size="icon"
                            variant="outline"
                            className="h-10 w-10 rounded-full border-hairline-strong bg-card text-foreground shadow-sm hover:bg-surface-strong"
                            aria-label="动态变量"
                            onClick={openDynamicVariables}
                          >
                            <Braces size={17} />
                          </Button>
                        </TooltipTrigger>
                        <TooltipContent side="right">动态变量</TooltipContent>
                      </Tooltip>
                    </div>
                  </TooltipProvider>
                </Panel>
              </ReactFlow>
            </section>

            <AddNodeDialog
              open={addOpen}
              connectingFromSource={Boolean(addSourceId)}
              specs={addableSpecs}
              canAdd={canAddSpec}
              onOpenChange={(open) => {
                setAddOpen(open);
                if (!open) {
                  setAddSourceId(null);
                  setAddPosition(null);
                }
              }}
              onSelect={addNode}
            />

            {(debugOpen ||
              dynamicVariablesOpen ||
              settingsOpen ||
              (editingNode && editingSpec) ||
              editingEdge) && (
              <WorkflowSidePanel
                title={
                  debugOpen
                    ? undefined
                    : dynamicVariablesOpen
                      ? "动态变量"
                      : settingsOpen
                        ? "工作流设置"
                        : editingEdge
                          ? (
                              <EditableTitle
                                value={editingEdgeData?.label ?? ""}
                                placeholder={
                                  editingEdgeData?.mode === "llm"
                                    ? "大模型判断"
                                    : editingEdgeData?.mode === "expression"
                                      ? "表达式"
                                      : "默认路径"
                                }
                                editLabel="连接名称"
                                variant="panel"
                                allowEmpty
                                maxLength={64}
                                onChange={(label) =>
                                  updateEdgeData(editingEdge.id, {
                                    label: label || undefined,
                                  })
                                }
                              />
                            )
                          : editingNode && editingSpec
                            ? (
                                <EditableTitle
                                  value={
                                    (editingNode.data as WorkflowNodeData).name ??
                                    ""
                                  }
                                  placeholder={editingSpec.displayName}
                                  editLabel="节点名称"
                                  variant="panel"
                                  onChange={(name) =>
                                    updateNodeData(editingNode.id, { name })
                                  }
                                />
                              )
                            : "编辑节点"
                }
                closeLabel={
                  dynamicVariablesOpen
                    ? "关闭动态变量"
                    : settingsOpen
                      ? "关闭工作流设置"
                      : editingEdge
                        ? "关闭边编辑"
                        : "关闭节点编辑"
                }
                onClose={
                  debugOpen
                    ? undefined
                    : () => {
                        if (dynamicVariablesOpen) {
                          onDynamicVariablesOpenChange(false);
                        } else if (settingsOpen) setSettingsOpen(false);
                        else if (editingEdge) onEditingEdgeIdChange(null);
                        else onEditingNodeIdChange(null);
                      }
                }
              >
                {debugOpen ? (
                  debugPanel
                ) : dynamicVariablesOpen ? (
                  dynamicVariablesPanel
                ) : (
                    <div className="scrollbar-subtle min-h-0 flex-1 overflow-y-auto bg-canvas-soft px-4 pb-5 pt-4">
                      {settingsOpen ? (
                        <GlobalSettingsPanel
                          settings={settings}
                          onSettingsChange={onSettingsChange}
                          modelOptions={modelOptions}
                          toolOptions={toolOptions}
                          knowledgeOptions={knowledgeOptions}
                        />
                      ) : editingNode && editingSpec ? (
                        <NodeSettingsPanel
                          key={`${editingNode.id}:${(editingNode.data as WorkflowNodeData).name ?? ""}`}
                          panel
                          spec={editingSpec}
                          data={editingNode.data as WorkflowNodeData}
                          toolOptions={toolOptions}
                          knowledgeOptions={knowledgeOptions}
                          llmOptions={modelOptions.llm}
                          asrOptions={modelOptions.asr}
                          ttsOptions={modelOptions.tts}
                          visionOptions={modelOptions.vision}
                          workflowSettings={settings}
                          onChange={(patch) =>
                            updateNodeData(editingNode.id, patch)
                          }
                        />
                      ) : editingEdge ? (
                        <EdgeSettingsPanel
                          key={editingEdge.id}
                          edge={editingEdge}
                          sourceType={
                            nodes.find(
                              (node) => node.id === editingEdge.source,
                            )?.type as string | undefined
                          }
                          isOnlyOutgoing={
                            edges.filter(
                              (edge) => edge.source === editingEdge.source,
                            ).length === 1
                          }
                          hasOtherDefaultPath={hasDefaultPath(
                            edges,
                            editingEdge.source,
                            editingEdge.id,
                          )}
                          onChange={(patch) =>
                            updateEdgeData(editingEdge.id, patch)
                          }
                        />
                      ) : null}
                    </div>
                )}
              </WorkflowSidePanel>
            )}
          </div>
        </EdgeActionContext.Provider>
      </NodeActionContext.Provider>
      </ActiveNodeContext.Provider>
    </NodeSpecsContext.Provider>
  );
}
