"use client";

/**
 * 工作流可视化编辑器。基于 @xyflow/react,样式套用 ai-video 设计 token。
 *
 * 工具栏:添加节点(弹窗选类型)、工作流设置(ASR/LLM/TTS、是否允许打断)。
 * 节点与条件边:悬停/选中出现「编辑 / 删除」按钮,编辑统一用弹出对话框。
 * 节点目录来自后端 /api/node-types。数据通过 value/onChange 与外部 graph 同步。
 */

import {
  addEdge,
  Background,
  BackgroundVariant,
  type Connection,
  Controls,
  type Edge,
  MiniMap,
  type Node,
  Panel,
  ReactFlow,
  ReactFlowProvider,
  useEdgesState,
  useNodesState,
  useReactFlow,
} from "@xyflow/react";
import "@xyflow/react/dist/style.css";
import { Loader2, Plus, Settings2, Trash2 } from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";

import { Button } from "@/components/ui/button";
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";

import { edgeTypes } from "./ConditionEdge";
import {
  EdgeActionContext,
  NodeActionContext,
  NodeSpecsContext,
} from "./context";
import { nodeTypes } from "./GenericNode";
import {
  accentVar,
  defaultGraph,
  type NodeSpecMap,
  type RuntimeNodeSpec,
  type WorkflowGraph,
  type WorkflowNodeData,
  type WorkflowNodeType,
} from "./specs";
import { useNodeSpecs } from "./useNodeSpecs";

export type WorkflowSettings = {
  llm?: string;
  asr?: string;
  tts?: string;
  allowInterrupt: boolean;
};

export type ModelOption = { value: string; label: string };

export type WorkflowEditorProps = {
  value?: WorkflowGraph;
  onChange?: (graph: WorkflowGraph) => void;
  settings: WorkflowSettings;
  onSettingsChange: (settings: WorkflowSettings) => void;
  modelOptions: { llm: ModelOption[]; asr: ModelOption[]; tts: ModelOption[] };
};

let nodeSeq = 0;
const NONE = "__none__";

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 {
    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 as { condition?: string; label?: string }) ?? {},
    })),
  };
}

function Canvas({
  value,
  onChange,
  settings,
  onSettingsChange,
  modelOptions,
  specsByType,
}: 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 [editingId, setEditingId] = useState<string | null>(null);
  const [editingEdgeId, setEditingEdgeId] = useState<string | null>(null);
  const [addOpen, setAddOpen] = useState(false);
  const [settingsOpen, setSettingsOpen] = useState(false);
  const { screenToFlowPosition } = useReactFlow();

  // 回传画布状态给外部(助手 graph)。用 ref 避免把 onChange 放进依赖导致循环。
  const onChangeRef = useRef(onChange);
  useEffect(() => {
    onChangeRef.current = onChange;
  }, [onChange]);
  useEffect(() => {
    onChangeRef.current?.(fromFlow(nodes, edges));
  }, [nodes, edges]);

  const onConnect = useCallback(
    (connection: Connection) => {
      setEdges((eds) =>
        addEdge(
          {
            ...connection,
            id: `e-${connection.source}-${connection.target}-${Date.now()}`,
            type: "condition",
            animated: true,
            data: {},
          },
          eds,
        ),
      );
    },
    [setEdges],
  );

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

  const addNode = useCallback(
    (spec: RuntimeNodeSpec) => {
      nodeSeq += 1;
      const id = `${spec.type}-${Date.now()}-${nodeSeq}`;
      const position = screenToFlowPosition({
        x: window.innerWidth / 2,
        y: window.innerHeight / 2,
      });
      const data: WorkflowNodeData = { name: spec.displayName };
      if (spec.type === "agentNode") {
        data.allowInterrupt = true;
        data.prompt = "";
      }
      setNodes((ns) => [...ns, { id, type: spec.type, position, data }]);
      setAddOpen(false);
      setEditingId(id);
    },
    [screenToFlowPosition, setNodes],
  );

  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) => {
      setNodes((ns) => ns.filter((n) => n.id !== id));
      setEdges((es) => es.filter((e) => e.source !== id && e.target !== id));
      setEditingId((cur) => (cur === id ? null : cur));
    },
    [setNodes, setEdges],
  );

  const updateEdgeData = useCallback(
    (id: string, patch: { condition?: string; label?: string }) => {
      setEdges((es) =>
        es.map((e) =>
          e.id === id ? { ...e, data: { ...(e.data ?? {}), ...patch } } : e,
        ),
      );
    },
    [setEdges],
  );

  const deleteEdge = useCallback(
    (id: string) => {
      setEdges((es) => es.filter((e) => e.id !== id));
      setEditingEdgeId((cur) => (cur === id ? null : cur));
    },
    [setEdges],
  );

  const nodeActions = useMemo(
    () => ({ edit: setEditingId, remove: deleteNode }),
    [deleteNode],
  );
  const edgeActions = useMemo(
    () => ({ edit: setEditingEdgeId, remove: deleteEdge }),
    [deleteEdge],
  );

  const editingNode = nodes.find((n) => n.id === editingId);
  const editingSpec = editingNode ? specsByType[editingNode.type as string] : null;
  const editingEdge = edges.find((e) => e.id === editingEdgeId);
  const addableSpecs = Object.values(specsByType).filter((s) => s.addable);

  return (
    <NodeSpecsContext.Provider value={specsByType}>
      <NodeActionContext.Provider value={nodeActions}>
        <EdgeActionContext.Provider value={edgeActions}>
          <div className="h-full w-full overflow-hidden rounded-2xl border border-hairline bg-canvas-soft">
            <ReactFlow
              nodes={nodes}
              edges={edges}
              nodeTypes={nodeTypes}
              edgeTypes={edgeTypes}
              onNodesChange={onNodesChange}
              onEdgesChange={onEdgesChange}
              onConnect={onConnect}
              isValidConnection={isValidConnection}
              onPaneClick={() => {
                setEditingId(null);
                setEditingEdgeId(null);
              }}
              fitView
              proOptions={{ hideAttribution: true }}
              defaultEdgeOptions={{ type: "condition", animated: true }}
              className="bg-canvas-soft"
            >
              <Background
                variant={BackgroundVariant.Dots}
                gap={20}
                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" />
              <MiniMap
                pannable
                zoomable
                className="!rounded-xl !border !border-hairline !bg-card"
                maskColor="color-mix(in srgb, var(--canvas-soft) 70%, transparent)"
                nodeColor="var(--surface-strong)"
              />
              <Panel position="top-left" className="flex gap-2">
                <Button size="sm" className="gap-2" onClick={() => setAddOpen(true)}>
                  <Plus size={15} />
                  添加节点
                </Button>
                <Button
                  size="sm"
                  variant="outline"
                  className="gap-2 border-hairline-strong bg-card text-foreground hover:bg-surface-strong"
                  onClick={() => setSettingsOpen(true)}
                >
                  <Settings2 size={15} />
                  工作流设置
                </Button>
              </Panel>
            </ReactFlow>

            {/* 添加节点弹窗 */}
            <Dialog open={addOpen} onOpenChange={setAddOpen}>
              <DialogContent className="sm:max-w-[460px]">
                <DialogHeader>
                  <DialogTitle className="font-display text-ink">
                    添加节点
                  </DialogTitle>
                  <DialogDescription>
                    选择要添加到画布的节点类型。
                  </DialogDescription>
                </DialogHeader>
                <div className="flex flex-col gap-2 py-2">
                  {addableSpecs.length === 0 && (
                    <p className="text-sm text-muted-soft">暂无可添加的节点类型。</p>
                  )}
                  {addableSpecs.map((spec) => {
                    const Icon = spec.icon;
                    return (
                      <button
                        key={spec.type}
                        type="button"
                        className="flex items-start gap-3 rounded-xl border border-hairline bg-card p-3 text-left transition-colors hover:border-primary hover:bg-surface-strong/40"
                        onClick={() => addNode(spec)}
                      >
                        <div
                          className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl text-ink"
                          style={{
                            backgroundImage: `radial-gradient(circle at 30% 30%, color-mix(in srgb, var(${accentVar(spec.accent)}) 70%, transparent), color-mix(in srgb, var(${accentVar(spec.accent)}) 35%, transparent))`,
                          }}
                        >
                          <Icon size={18} />
                        </div>
                        <div className="min-w-0">
                          <div className="text-sm font-medium text-foreground">
                            {spec.displayName}
                          </div>
                          <div className="mt-0.5 text-xs leading-5 text-muted-foreground">
                            {spec.description}
                          </div>
                        </div>
                      </button>
                    );
                  })}
                </div>
              </DialogContent>
            </Dialog>

            {/* 工作流设置弹窗 */}
            <Dialog open={settingsOpen} onOpenChange={setSettingsOpen}>
              <DialogContent className="sm:max-w-[460px]">
                <DialogHeader>
                  <DialogTitle className="font-display text-ink">
                    工作流设置
                  </DialogTitle>
                  <DialogDescription>
                    配置整个工作流使用的语音与大模型,以及交互策略。
                  </DialogDescription>
                </DialogHeader>
                <div className="flex flex-col gap-5 py-2">
                  <ModelSelect
                    label="大模型(LLM)"
                    value={settings.llm}
                    options={modelOptions.llm}
                    onChange={(v) => onSettingsChange({ ...settings, llm: v })}
                  />
                  <ModelSelect
                    label="语音识别(ASR)"
                    value={settings.asr}
                    options={modelOptions.asr}
                    onChange={(v) => onSettingsChange({ ...settings, asr: v })}
                  />
                  <ModelSelect
                    label="语音合成(TTS)"
                    value={settings.tts}
                    options={modelOptions.tts}
                    onChange={(v) => onSettingsChange({ ...settings, tts: v })}
                  />
                  <label className="flex items-center justify-between gap-3 pt-1">
                    <span className="text-sm font-medium text-foreground">
                      允许用户打断
                    </span>
                    <Switch
                      checked={settings.allowInterrupt}
                      onCheckedChange={(checked) =>
                        onSettingsChange({ ...settings, allowInterrupt: checked })
                      }
                    />
                  </label>
                </div>
                <DialogFooter>
                  <Button onClick={() => setSettingsOpen(false)}>完成</Button>
                </DialogFooter>
              </DialogContent>
            </Dialog>

            {/* 节点编辑弹窗 */}
            <Dialog
              open={!!editingNode}
              onOpenChange={(open) => !open && setEditingId(null)}
            >
              <DialogContent className="sm:max-w-[460px]">
                {editingNode && editingSpec && (
                  <NodeForm
                    key={editingNode.id}
                    spec={editingSpec}
                    data={editingNode.data as WorkflowNodeData}
                    onSave={(patch) => {
                      updateNodeData(editingNode.id, patch);
                      setEditingId(null);
                    }}
                    onDelete={
                      editingSpec.addable
                        ? () => deleteNode(editingNode.id)
                        : undefined
                    }
                  />
                )}
              </DialogContent>
            </Dialog>

            {/* 条件编辑弹窗 */}
            <Dialog
              open={!!editingEdge}
              onOpenChange={(open) => !open && setEditingEdgeId(null)}
            >
              <DialogContent className="sm:max-w-[460px]">
                {editingEdge && (
                  <EdgeForm
                    key={editingEdge.id}
                    edge={editingEdge}
                    onSave={(patch) => {
                      updateEdgeData(editingEdge.id, patch);
                      setEditingEdgeId(null);
                    }}
                    onDelete={() => deleteEdge(editingEdge.id)}
                  />
                )}
              </DialogContent>
            </Dialog>
          </div>
        </EdgeActionContext.Provider>
      </NodeActionContext.Provider>
    </NodeSpecsContext.Provider>
  );
}

function ModelSelect({
  label,
  value,
  options,
  onChange,
}: {
  label: string;
  value?: string;
  options: ModelOption[];
  onChange: (value: string | undefined) => void;
}) {
  return (
    <div className="flex flex-col gap-2">
      <label className="text-sm font-medium text-foreground">{label}</label>
      <Select
        value={value ?? NONE}
        onValueChange={(v) => onChange(v === NONE ? undefined : v)}
      >
        <SelectTrigger className="border-hairline-strong bg-background text-foreground">
          <SelectValue placeholder="选择模型资源" />
        </SelectTrigger>
        <SelectContent>
          <SelectItem value={NONE}>未选择</SelectItem>
          {options.map((o) => (
            <SelectItem key={o.value} value={o.value}>
              {o.label}
            </SelectItem>
          ))}
        </SelectContent>
      </Select>
    </div>
  );
}

function NodeForm({
  spec,
  data,
  onSave,
  onDelete,
}: {
  spec: RuntimeNodeSpec;
  data: WorkflowNodeData;
  onSave: (patch: WorkflowNodeData) => void;
  onDelete?: () => void;
}) {
  const [draft, setDraft] = useState<WorkflowNodeData>({ ...data });
  const set = (key: string, val: unknown) =>
    setDraft((d) => ({ ...d, [key]: val }));

  return (
    <>
      <DialogHeader>
        <DialogTitle className="font-display text-ink">
          编辑{spec.displayName}
        </DialogTitle>
        <DialogDescription>{spec.description}</DialogDescription>
      </DialogHeader>

      <div className="flex flex-col gap-5 py-2">
        {spec.fields.map((field) => {
          const raw = draft[field.key];
          if (field.type === "switch") {
            return (
              <label
                key={field.key}
                className="flex items-center justify-between gap-3"
              >
                <span className="text-sm font-medium text-foreground">
                  {field.label}
                </span>
                <Switch
                  checked={Boolean(raw)}
                  onCheckedChange={(checked) => set(field.key, checked)}
                />
              </label>
            );
          }
          return (
            <div key={field.key} className="flex flex-col gap-2">
              <label className="text-sm font-medium text-foreground">
                {field.label}
                {field.required && <span className="text-destructive"> *</span>}
              </label>
              {field.type === "textarea" ? (
                <Textarea
                  rows={4}
                  value={(raw as string) ?? ""}
                  onChange={(e) => set(field.key, e.target.value)}
                  className="resize-none border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
                />
              ) : (
                <Input
                  value={(raw as string) ?? ""}
                  onChange={(e) => set(field.key, e.target.value)}
                  className="border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
                />
              )}
            </div>
          );
        })}
      </div>

      <DialogFooter className="flex-row justify-between sm:justify-between">
        {onDelete ? (
          <Button
            variant="outline"
            className="gap-2 border-hairline-strong text-muted-foreground hover:text-destructive"
            onClick={onDelete}
          >
            <Trash2 size={15} />
            删除节点
          </Button>
        ) : (
          <span />
        )}
        <Button onClick={() => onSave(draft)}>保存</Button>
      </DialogFooter>
    </>
  );
}

function EdgeForm({
  edge,
  onSave,
  onDelete,
}: {
  edge: Edge;
  onSave: (patch: { condition: string; label: string }) => void;
  onDelete: () => void;
}) {
  const data = (edge.data ?? {}) as { condition?: string; label?: string };
  const [label, setLabel] = useState(data.label ?? "");
  const [condition, setCondition] = useState(data.condition ?? "");

  return (
    <>
      <DialogHeader>
        <DialogTitle className="font-display text-ink">编辑条件</DialogTitle>
        <DialogDescription>
          条件用于让大模型判断对话是否应沿这条路径流转到下一节点。
        </DialogDescription>
      </DialogHeader>

      <div className="flex flex-col gap-5 py-2">
        <div className="flex flex-col gap-2">
          <label className="text-sm font-medium text-foreground">条件标签</label>
          <Input
            value={label}
            maxLength={64}
            placeholder="例如:用户想转人工"
            onChange={(e) => setLabel(e.target.value)}
            className="border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
          />
          <span className="text-xs text-muted-soft">
            日志中识别该路径的短标签,{label.length}/64
          </span>
        </div>

        <div className="flex flex-col gap-2">
          <label className="text-sm font-medium text-foreground">条件</label>
          <Textarea
            rows={4}
            value={condition}
            placeholder="描述触发这条路径的条件,例如:用户明确表示要找人工客服。"
            onChange={(e) => setCondition(e.target.value)}
            className="resize-none border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
          />
        </div>
      </div>

      <DialogFooter className="flex-row justify-between sm:justify-between">
        <Button
          variant="outline"
          className="gap-2 border-hairline-strong text-muted-foreground hover:text-destructive"
          onClick={onDelete}
        >
          <Trash2 size={15} />
          删除连线
        </Button>
        <Button onClick={() => onSave({ condition, label })}>保存条件</Button>
      </DialogFooter>
    </>
  );
}

export function WorkflowEditor(props: WorkflowEditorProps) {
  const { byType, loading, error } = useNodeSpecs();

  if (loading) {
    return (
      <div className="flex h-full w-full items-center justify-center rounded-2xl border border-hairline bg-canvas-soft text-muted-foreground">
        <Loader2 className="mr-2 animate-spin" size={18} />
        正在加载节点目录…
      </div>
    );
  }
  if (error) {
    return (
      <div className="flex h-full w-full items-center justify-center rounded-2xl border border-hairline bg-canvas-soft px-6 text-center text-sm text-destructive">
        加载节点目录失败:{error}
      </div>
    );
  }

  return (
    <ReactFlowProvider>
      <Canvas {...props} specsByType={byType} />
    </ReactFlowProvider>
  );
}
