'use client';

import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
  Background,
  ConnectionLineType,
  Controls,
  Handle,
  MiniMap,
  Position,
  ReactFlow,
  ReactFlowProvider,
  useReactFlow,
  useViewport,
  type Connection,
  type Edge,
  type EdgeChange,
  type Node as FlowNode,
  type NodeChange,
  type NodeProps,
  type Rect,
  type ReactFlowInstance,
  type Viewport as FlowViewport,
} from '@xyflow/react';
import { useTheme } from 'next-themes';
import '@xyflow/react/dist/style.css';
import { Brush, Download, Eye, FileImage, Image as ImageIcon, Layers, Link2, Loader2, Maximize2, Move, Sparkles, Type, Wand2 } from 'lucide-react';
import { cn } from '@/lib/utils';
import type { CanvasConnection, CanvasLayer, CanvasNode, CanvasViewport } from '@/lib/canvas-types';

type NodePositionCommit = { id: string; x: number; y: number };
type CanvasModelOption = { id: string; label: string };

type CanvasFlowNodeData = {
  node: CanvasNode;
  selected: boolean;
  connecting: boolean;
  connections: CanvasConnection[];
  allNodes: CanvasNode[];
  connectingFromId: string | null;
  imageModelOptions: CanvasModelOption[];
  textModelOptions: CanvasModelOption[];
  layerCanvasSize: number;
  layerColors: Record<CanvasLayer['type'], string>;
  onSelect: (id: string, additive: boolean) => void;
  onConnect: (sourceId: string, targetId: string) => void;
  onStartConnect: (id: string) => void;
  onUpdateNode: (id: string, patch: Partial<CanvasNode>) => void;
  onRunNode: (id: string) => void;
  onRunDownstreamNode: (id: string) => void;
  onCreateVariationNode: (id: string) => void;
  onPreviewNode: (id: string) => void;
  onDownloadNode: (id: string) => void;
  onRemoveConnection: (id: string) => void;
};

type CanvasFlowNode = FlowNode<CanvasFlowNodeData, 'canvasNode'>;

type ReactFlowCanvasProps = {
  nodes: CanvasNode[];
  connections: CanvasConnection[];
  imageModelOptions: CanvasModelOption[];
  textModelOptions: CanvasModelOption[];
  viewport: CanvasViewport;
  selectedNodeIds: string[];
  connectingFromId: string | null;
  editable: boolean;
  minZoom: number;
  maxZoom: number;
  layerCanvasSize: number;
  layerColors: Record<CanvasLayer['type'], string>;
  onSelectNode: (id: string, additive: boolean) => void;
  onSelectionChange: (ids: string[]) => void;
  onStartConnect: (id: string) => void;
  onUpdateNode: (id: string, patch: Partial<CanvasNode>) => void;
  onRunNode: (id: string) => void;
  onRunDownstreamNode: (id: string) => void;
  onCreateVariationNode: (id: string) => void;
  onPreviewNode: (id: string) => void;
  onDownloadNode: (id: string) => void;
  onConnect: (sourceId: string, targetId: string) => void;
  onRemoveConnection: (id: string) => void;
  onRemoveConnections: (ids: string[]) => void;
  onNodesCommit: (positions: NodePositionCommit[], options?: { history?: boolean; dirty?: boolean }) => void;
  onViewportCommit: (viewport: CanvasViewport) => void;
  onReady?: (controls: CanvasFlowControls | null) => void;
  onPaneClick: (point: { x: number; y: number }, event: MouseEvent | React.MouseEvent) => void;
  onPaneDoubleClick: (point: { x: number; y: number }, event: MouseEvent | React.MouseEvent) => void;
};

export type CanvasFlowControls = {
  setViewport: (viewport: CanvasViewport) => Promise<boolean>;
  zoomTo: (zoom: number) => Promise<boolean>;
  fitBounds: (bounds: Rect, options?: { padding?: number; duration?: number }) => Promise<boolean>;
  getViewport: () => CanvasViewport;
};

const SNAP_GRID: [number, number] = [20, 20];
const MULTI_SELECTION_KEYS = ['Meta', 'Control', 'Shift'];
const FLOW_FIT_VIEW_OPTIONS = { padding: 0.2 };
const CANVAS_NODE_TYPES = { canvasNode: CanvasNodeCard };
const NODE_TYPE_META: Record<CanvasNode['type'], { label: string; accent: string; bg: string }> = {
  text: { label: 'TEXT', accent: '#2563eb', bg: 'rgba(37, 99, 235, 0.1)' },
  image: { label: 'ASSET', accent: '#2563eb', bg: 'rgba(37, 99, 235, 0.1)' },
  text2img: { label: 'T2I', accent: '#7c3aed', bg: 'rgba(124, 58, 237, 0.1)' },
  img2img: { label: 'I2I', accent: '#db2777', bg: 'rgba(219, 39, 119, 0.1)' },
  layeredImage: { label: 'PSD', accent: '#ca8a04', bg: 'rgba(202, 138, 4, 0.12)' },
  frame: { label: 'GROUP', accent: '#7c3aed', bg: 'rgba(124, 58, 237, 0.08)' },
};
const TEXT_IMAGE_ASPECT_OPTIONS = ['1:1', '16:9', '9:16', '4:3', '3:4'];
const IMAGE_REFERENCE_ASPECT_OPTIONS = ['original', ...TEXT_IMAGE_ASPECT_OPTIONS];
const RESOLUTION_OPTIONS = ['1080P', '2K', '4K'];

function getNodeImageUrl(node?: CanvasNode | null) {
  if (!node) return '';
  if (node.type === 'image') return node.imageUrl || '';
  return node.selectedOutput || node.outputImages?.[0] || '';
}

function getNodeTextValue(node?: CanvasNode | null) {
  if (!node || node.type !== 'text') return '';
  return node.text?.trim() || '';
}

function NodeOutputStrip({
  node,
  onUpdateNode,
}: {
  node: CanvasNode;
  onUpdateNode: (id: string, patch: Partial<CanvasNode>) => void;
}) {
  const outputs = node.outputImages || [];
  if (outputs.length <= 1) return null;
  const activeOutput = node.selectedOutput || outputs[0];
  return (
    <div className="nodrag absolute bottom-2 left-2 right-2 z-20 flex gap-1 overflow-x-auto rounded-md border bg-background/90 p-1 shadow-sm backdrop-blur">
      {outputs.map((src, index) => (
        <button
          key={`${src}-${index}`}
          type="button"
          className={cn(
            'h-10 w-10 shrink-0 overflow-hidden rounded border bg-muted transition-all hover:border-primary',
            activeOutput === src ? 'border-primary ring-2 ring-primary/30' : 'border-border',
          )}
          title={`选择结果 ${index + 1}`}
          onPointerDown={(event) => event.stopPropagation()}
          onClick={(event) => {
            event.stopPropagation();
            onUpdateNode(node.id, {
              selectedOutput: src,
              ...(node.type === 'image' ? { imageUrl: src } : {}),
            });
          }}
        >
          {/* eslint-disable-next-line @next/next/no-img-element */}
          <img src={src} alt={`结果 ${index + 1}`} className="h-full w-full object-cover" />
        </button>
      ))}
    </div>
  );
}

function NodeCountControl({
  node,
  onUpdateNode,
}: {
  node: CanvasNode;
  onUpdateNode: (id: string, patch: Partial<CanvasNode>) => void;
}) {
  const count = Math.min(4, Math.max(1, node.params?.count || 1));
  return (
    <div className="nodrag flex shrink-0 rounded-md border bg-muted/30 p-0.5" title="生成张数">
      {[1, 2, 4].map(value => (
        <button
          key={value}
          type="button"
          className={cn(
            'h-6 min-w-6 rounded px-1.5 text-[11px] font-medium transition-colors',
            count === value ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:bg-background hover:text-foreground',
          )}
          onPointerDown={(event) => event.stopPropagation()}
          onClick={(event) => {
            event.stopPropagation();
            onUpdateNode(node.id, { params: { ...node.params, count: value } });
          }}
        >
          {value}
        </button>
      ))}
    </div>
  );
}

function NodeInlineParams({
  node,
  hasReference,
  imageModelOptions,
  onUpdateNode,
}: {
  node: CanvasNode;
  hasReference: boolean;
  imageModelOptions: CanvasModelOption[];
  onUpdateNode: (id: string, patch: Partial<CanvasNode>) => void;
}) {
  const aspectOptions = node.type === 'text2img' || (node.type === 'image' && !hasReference)
    ? TEXT_IMAGE_ASPECT_OPTIONS
    : IMAGE_REFERENCE_ASPECT_OPTIONS;
  const aspectRatio = node.params?.aspectRatio && aspectOptions.includes(node.params.aspectRatio)
    ? node.params.aspectRatio
    : aspectOptions[0];
  const resolution = node.params?.resolution && RESOLUTION_OPTIONS.includes(node.params.resolution)
    ? node.params.resolution
    : '2K';
  const modelValue = node.params?.model || imageModelOptions[0]?.id || '';
  return (
    <div className="nodrag mt-2 grid grid-cols-[minmax(0,1.2fr)_72px_72px] gap-2">
      <select
        className="h-7 min-w-0 rounded-md border bg-muted/30 px-2 text-[11px] text-foreground outline-none transition-colors focus:border-primary focus:bg-background disabled:cursor-not-allowed disabled:opacity-60"
        value={modelValue}
        title="生成模型"
        disabled={imageModelOptions.length === 0}
        onPointerDown={(event) => event.stopPropagation()}
        onChange={(event) => onUpdateNode(node.id, { params: { ...node.params, model: event.target.value } })}
      >
        {imageModelOptions.length > 0 ? imageModelOptions.map(option => (
          <option key={option.id} value={option.id}>{option.label}</option>
        )) : <option value="">无可用模型</option>}
      </select>
      <select
        className="h-7 min-w-0 rounded-md border bg-muted/30 px-2 text-[11px] text-foreground outline-none transition-colors focus:border-primary focus:bg-background"
        value={aspectRatio}
        title="画面比例"
        onPointerDown={(event) => event.stopPropagation()}
        onChange={(event) => onUpdateNode(node.id, { params: { ...node.params, aspectRatio: event.target.value } })}
      >
        {aspectOptions.map(value => (
          <option key={value} value={value}>{value === 'original' ? '原比例' : value}</option>
        ))}
      </select>
      <select
        className="h-7 min-w-0 rounded-md border bg-muted/30 px-2 text-[11px] text-foreground outline-none transition-colors focus:border-primary focus:bg-background"
        value={resolution}
        title="分辨率"
        onPointerDown={(event) => event.stopPropagation()}
        onChange={(event) => onUpdateNode(node.id, { params: { ...node.params, resolution: event.target.value } })}
      >
        {RESOLUTION_OPTIONS.map(value => (
          <option key={value} value={value}>{value}</option>
        ))}
      </select>
    </div>
  );
}

function CanvasNodeCard({ data }: NodeProps<CanvasFlowNode>) {
  const { node, selected, connecting, connections, allNodes, connectingFromId, imageModelOptions, textModelOptions, layerCanvasSize, layerColors, onSelect, onConnect, onStartConnect, onUpdateNode, onRunNode, onRunDownstreamNode, onCreateVariationNode, onPreviewNode, onDownloadNode } = data;
  const incomingNodes = allNodes.filter(item => connections.some(connection => connection.targetNodeId === node.id && connection.sourceNodeId === item.id));
  const incomingImage = incomingNodes.find(item => !!getNodeImageUrl(item));
  const incomingText = incomingNodes.map(getNodeTextValue).filter(Boolean).join('\n');
  const hasOutgoing = connections.some(connection => connection.sourceNodeId === node.id);
  const meta = NODE_TYPE_META[node.type];
  const [editingText, setEditingText] = useState(false);
  const [editingNegativePrompt, setEditingNegativePrompt] = useState(false);
  const imageSrc = node.imageUrl || node.selectedOutput || node.outputImages?.[0] || '';
  const generationImageSrc = node.selectedOutput || node.outputImages?.[0] || '';
  const hasUsableImage = !!getNodeImageUrl(node);
  const hasReferenceImage = !!(node.referenceImage || getNodeImageUrl(incomingImage));

  return (
    <div
      data-canvas-node
      data-node-id={node.id}
      className={cn(
        'group h-full overflow-visible rounded-lg border transition-shadow',
        node.type === 'frame' ? 'border-dashed bg-background/10 shadow-none' : 'bg-card shadow-md',
        selected ? 'border-primary ring-2 ring-primary/30' : connecting ? 'border-primary ring-2 ring-primary/25' : 'border-border',
      )}
      style={{ borderColor: node.type === 'frame' && !selected ? node.color || meta.accent : undefined }}
      onPointerDown={(event) => {
        const target = event.target as HTMLElement;
        if (target.closest('button,input,textarea,[role="combobox"],.nodrag')) return;
        if (connectingFromId && connectingFromId !== node.id) {
          event.preventDefault();
          onConnect(connectingFromId, node.id);
          return;
        }
        onSelect(node.id, event.ctrlKey || event.metaKey || event.shiftKey);
      }}
    >
      <Handle
        type="target"
        position={Position.Left}
        id="input"
        className="nodrag nopan !left-[-9px] !z-30 !h-5 !w-5 !border-2 !border-background !opacity-100"
        style={{ backgroundColor: meta.accent }}
      />
      <span className="nodrag pointer-events-none absolute -left-9 top-1/2 z-20 -translate-y-1/2 rounded-full border bg-background px-1.5 py-0.5 text-[9px] font-semibold text-muted-foreground shadow-sm">IN</span>
      <Handle
        type="source"
        position={Position.Right}
        id="output"
        className="nodrag nopan !right-[-9px] !z-30 !h-5 !w-5 !border-2 !border-background !opacity-100"
        style={{ backgroundColor: meta.accent }}
      />
      <span className="nodrag pointer-events-none absolute -right-11 top-1/2 z-20 -translate-y-1/2 rounded-full border bg-background px-1.5 py-0.5 text-[9px] font-semibold text-muted-foreground shadow-sm">OUT</span>

      <button
        type="button"
        className={cn(
          'nodrag absolute -right-3 top-3 z-20 flex h-7 w-7 items-center justify-center rounded-full border bg-background text-muted-foreground opacity-0 shadow-sm transition-all hover:border-primary hover:text-primary group-hover:opacity-100',
          connecting ? 'border-primary bg-primary text-primary-foreground opacity-100' : '',
        )}
        title="从此模块开始连线"
        onPointerDown={(event) => event.stopPropagation()}
        onClick={(event) => {
          event.stopPropagation();
          onStartConnect(node.id);
        }}
      >
        <Link2 className="h-3.5 w-3.5" />
      </button>

      {hasOutgoing ? (
        <button
          type="button"
          className="nodrag absolute -right-3 top-12 z-20 flex h-7 w-7 items-center justify-center rounded-full border bg-background text-muted-foreground opacity-0 shadow-sm transition-all hover:border-primary hover:text-primary group-hover:opacity-100"
          title="运行下游"
          onPointerDown={(event) => event.stopPropagation()}
          onClick={(event) => {
            event.stopPropagation();
            onRunDownstreamNode(node.id);
          }}
        >
          <Sparkles className="h-3.5 w-3.5" />
        </button>
      ) : null}

      {hasUsableImage ? (
        <button
          type="button"
          className="nodrag absolute -right-3 top-[5.25rem] z-20 flex h-7 w-7 items-center justify-center rounded-full border bg-background text-muted-foreground opacity-0 shadow-sm transition-all hover:border-primary hover:text-primary group-hover:opacity-100"
          title="用当前图继续创作"
          onPointerDown={(event) => event.stopPropagation()}
          onClick={(event) => {
            event.stopPropagation();
            onCreateVariationNode(node.id);
          }}
        >
          <Wand2 className="h-3.5 w-3.5" />
        </button>
      ) : null}

      <div className="h-full overflow-hidden rounded-lg">
        <div className="h-1.5" style={{ backgroundColor: meta.accent }} />
        <div className="flex h-11 items-center justify-between border-b bg-muted/40 px-3">
          <div className="flex min-w-0 items-center gap-2 text-sm font-medium">
            {node.type === 'text' ? <Type className="h-4 w-4" /> : null}
            {node.type === 'image' ? <ImageIcon className="h-4 w-4" /> : null}
            {node.type === 'text2img' ? <Brush className="h-4 w-4" /> : null}
            {node.type === 'img2img' ? <FileImage className="h-4 w-4" /> : null}
            {node.type === 'layeredImage' ? <Layers className="h-4 w-4" /> : null}
            {node.type === 'frame' ? <Maximize2 className="h-4 w-4" /> : null}
            <span className="truncate">{node.title}</span>
          </div>
          <div className="flex items-center gap-2">
            <span className="rounded-full px-2 py-0.5 text-[10px] font-semibold" style={{ color: meta.accent, backgroundColor: meta.bg }}>{meta.label}</span>
            {node.status === 'generating' ? <Loader2 className="h-4 w-4 animate-spin text-primary" /> : <Move className="h-4 w-4 text-muted-foreground" />}
          </div>
        </div>

        <div className="h-[calc(100%-2.875rem-0.375rem)] p-3">
          {node.type === 'text' ? (
            <div className="flex h-full flex-col gap-2">
              {editingText ? (
                <textarea
                  className="nodrag nowheel min-h-0 flex-1 resize-none rounded-md border border-primary/40 bg-background p-3 text-sm leading-6 outline-none ring-2 ring-primary/20"
                  value={node.text || ''}
                  autoFocus
                  onPointerDown={(event) => event.stopPropagation()}
                  onDoubleClick={(event) => event.stopPropagation()}
                  onChange={(event) => onUpdateNode(node.id, { text: event.target.value })}
                  onBlur={() => setEditingText(false)}
                  onKeyDown={(event) => {
                    if (event.key === 'Escape') {
                      event.preventDefault();
                      setEditingText(false);
                    }
                  }}
                />
              ) : (
                <button
                  type="button"
                  className="nodrag min-h-0 flex-1 whitespace-pre-wrap rounded-md bg-muted/30 p-3 text-left text-sm leading-6 text-foreground transition-colors hover:bg-muted/50"
                  onPointerDown={(event) => event.stopPropagation()}
                  onDoubleClick={(event) => {
                    event.stopPropagation();
                    setEditingText(true);
                  }}
                >
                  {node.text || '双击输入文本'}
                </button>
              )}
              <select
                className="nodrag h-7 shrink-0 rounded-md border bg-muted/30 px-2 text-[11px] text-foreground outline-none transition-colors focus:border-primary focus:bg-background disabled:cursor-not-allowed disabled:opacity-60"
                value={node.params?.model || textModelOptions[0]?.id || ''}
                title="多模态模型"
                disabled={textModelOptions.length === 0}
                onPointerDown={(event) => event.stopPropagation()}
                onChange={(event) => onUpdateNode(node.id, { params: { ...node.params, model: event.target.value } })}
              >
                {textModelOptions.length > 0 ? textModelOptions.map(option => (
                  <option key={option.id} value={option.id}>{option.label}</option>
                )) : <option value="">无可用多模态模型</option>}
              </select>
            </div>
          ) : null}

          {node.type === 'image' ? (
            <div className="flex h-full flex-col gap-2">
              <div className="relative min-h-0 flex-1 overflow-hidden rounded-md border bg-muted">
                {imageSrc ? (
                  <>
                    {/* eslint-disable-next-line @next/next/no-img-element */}
                    <img src={imageSrc} alt={node.title} className="h-full w-full object-contain" />
                    <div className="nodrag absolute right-2 top-2 flex gap-1 opacity-0 transition-opacity group-hover:opacity-100">
                      <button
                        type="button"
                        className="flex h-7 w-7 items-center justify-center rounded-md border bg-background/90 text-muted-foreground shadow-sm hover:text-foreground"
                        title="预览图片"
                        onPointerDown={(event) => event.stopPropagation()}
                        onClick={(event) => {
                          event.stopPropagation();
                          onPreviewNode(node.id);
                        }}
                      >
                        <Eye className="h-3.5 w-3.5" />
                      </button>
                      <button
                        type="button"
                        className="flex h-7 w-7 items-center justify-center rounded-md border bg-background/90 text-muted-foreground shadow-sm hover:text-foreground"
                        title="下载图片"
                        onPointerDown={(event) => event.stopPropagation()}
                        onClick={(event) => {
                          event.stopPropagation();
                          onDownloadNode(node.id);
                        }}
                      >
                        <Download className="h-3.5 w-3.5" />
                      </button>
                    </div>
                    <NodeOutputStrip node={node} onUpdateNode={onUpdateNode} />
                  </>
                ) : (
                  <div className="flex h-full items-center justify-center px-4 text-center text-sm text-muted-foreground">
                    {node.status === 'failed' ? node.error || '生成失败' : node.status === 'generating' ? '生成中...' : incomingText ? '已连接提示词，点击生成图片' : '连接文本节点或输入提示词'}
                  </div>
                )}
              </div>
              <div className="nodrag rounded-md border bg-background/95 p-2 shadow-sm">
                <textarea
                  className="nowheel h-14 w-full resize-none rounded-md border bg-muted/30 px-2 py-1.5 text-xs leading-5 outline-none transition-colors focus:border-primary focus:bg-background"
                  placeholder={incomingText ? '可补充描述；会叠加上游文本' : '输入创作描述，或从文本节点连到这里'}
                  value={node.prompt || ''}
                  onPointerDown={(event) => event.stopPropagation()}
                  onDoubleClick={(event) => event.stopPropagation()}
                  onChange={(event) => onUpdateNode(node.id, { prompt: event.target.value })}
                />
                <div className="mt-2 flex items-center justify-between gap-2">
                  <div className="min-w-0 truncate text-[11px] text-muted-foreground">
                    {incomingText ? `上游：${incomingText}` : incomingImage ? '已连接参考图' : '图片节点可直接生成'}
                  </div>
                  <NodeCountControl node={node} onUpdateNode={onUpdateNode} />
                  <button
                    type="button"
                    className="inline-flex h-7 shrink-0 items-center gap-1 rounded-md bg-primary px-2 text-xs font-medium text-primary-foreground disabled:cursor-not-allowed disabled:opacity-60"
                    disabled={node.status === 'generating'}
                    onPointerDown={(event) => event.stopPropagation()}
                    onClick={(event) => {
                      event.stopPropagation();
                      onRunNode(node.id);
                    }}
                  >
                    {node.status === 'generating' ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Sparkles className="h-3.5 w-3.5" />}
                    生成
                  </button>
                </div>
                <NodeInlineParams node={node} hasReference={hasReferenceImage} imageModelOptions={imageModelOptions} onUpdateNode={onUpdateNode} />
                <div className="mt-2">
                  {editingNegativePrompt ? (
                    <textarea
                      className="nowheel h-11 w-full resize-none rounded-md border bg-muted/30 px-2 py-1.5 text-xs leading-5 outline-none transition-colors focus:border-primary focus:bg-background"
                      placeholder="负面提示词，如：低清晰度、畸形、错误文字"
                      value={node.negativePrompt || ''}
                      autoFocus
                      onPointerDown={(event) => event.stopPropagation()}
                      onDoubleClick={(event) => event.stopPropagation()}
                      onChange={(event) => onUpdateNode(node.id, { negativePrompt: event.target.value })}
                      onBlur={() => {
                        if (!node.negativePrompt?.trim()) setEditingNegativePrompt(false);
                      }}
                    />
                  ) : (
                    <button
                      type="button"
                      className="w-full rounded-md border border-dashed bg-muted/20 px-2 py-1.5 text-left text-[11px] text-muted-foreground transition-colors hover:border-primary hover:text-foreground"
                      onPointerDown={(event) => event.stopPropagation()}
                      onClick={(event) => {
                        event.stopPropagation();
                        setEditingNegativePrompt(true);
                      }}
                    >
                      {node.negativePrompt?.trim() ? `负面：${node.negativePrompt}` : '添加负面提示词'}
                    </button>
                  )}
                </div>
              </div>
            </div>
          ) : null}

          {node.type === 'text2img' || node.type === 'img2img' ? (
            <div className="flex h-full flex-col gap-2">
              <div className="nodrag rounded-md border bg-background/95 p-2 shadow-sm">
                <textarea
                  className="nowheel h-14 w-full resize-none rounded-md border bg-muted/30 px-2 py-1.5 text-xs leading-5 outline-none transition-colors focus:border-primary focus:bg-background"
                  placeholder={incomingText ? '可补充描述；会叠加上游文本' : '输入创作描述，或从文本节点连到这里'}
                  value={node.prompt || ''}
                  onPointerDown={(event) => event.stopPropagation()}
                  onDoubleClick={(event) => event.stopPropagation()}
                  onChange={(event) => onUpdateNode(node.id, { prompt: event.target.value })}
                />
                <div className="mt-2 flex items-center justify-between gap-2">
                  <div className="min-w-0 truncate text-[11px] text-muted-foreground">
                    {incomingText ? `上游：${incomingText}` : node.type === 'img2img' && hasReferenceImage ? '已连接参考图' : '节点内可直接生成'}
                  </div>
                  <NodeCountControl node={node} onUpdateNode={onUpdateNode} />
                  <button
                    type="button"
                    className="inline-flex h-7 shrink-0 items-center gap-1 rounded-md bg-primary px-2 text-xs font-medium text-primary-foreground disabled:cursor-not-allowed disabled:opacity-60"
                    disabled={node.status === 'generating'}
                    onPointerDown={(event) => event.stopPropagation()}
                    onClick={(event) => {
                      event.stopPropagation();
                      onRunNode(node.id);
                    }}
                  >
                    {node.status === 'generating' ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Sparkles className="h-3.5 w-3.5" />}
                    生成
                  </button>
                </div>
                <NodeInlineParams node={node} hasReference={hasReferenceImage} imageModelOptions={imageModelOptions} onUpdateNode={onUpdateNode} />
                <div className="mt-2">
                  {editingNegativePrompt ? (
                    <textarea
                      className="nowheel h-11 w-full resize-none rounded-md border bg-muted/30 px-2 py-1.5 text-xs leading-5 outline-none transition-colors focus:border-primary focus:bg-background"
                      placeholder="负面提示词，如：低清晰度、畸形、错误文字"
                      value={node.negativePrompt || ''}
                      autoFocus
                      onPointerDown={(event) => event.stopPropagation()}
                      onDoubleClick={(event) => event.stopPropagation()}
                      onChange={(event) => onUpdateNode(node.id, { negativePrompt: event.target.value })}
                      onBlur={() => {
                        if (!node.negativePrompt?.trim()) setEditingNegativePrompt(false);
                      }}
                    />
                  ) : (
                    <button
                      type="button"
                      className="w-full rounded-md border border-dashed bg-muted/20 px-2 py-1.5 text-left text-[11px] text-muted-foreground transition-colors hover:border-primary hover:text-foreground"
                      onPointerDown={(event) => event.stopPropagation()}
                      onClick={(event) => {
                        event.stopPropagation();
                        setEditingNegativePrompt(true);
                      }}
                    >
                      {node.negativePrompt?.trim() ? `负面：${node.negativePrompt}` : '添加负面提示词'}
                    </button>
                  )}
                </div>
              </div>
              {node.type === 'img2img' ? (
                <div className="h-16 overflow-hidden rounded-md border bg-muted">
                  {node.referenceImage || getNodeImageUrl(incomingImage) ? (
                    // eslint-disable-next-line @next/next/no-img-element
                    <img src={node.referenceImage || getNodeImageUrl(incomingImage)} alt="参考图" className="h-full w-full object-contain" />
                  ) : (
                    <div className="flex h-full items-center justify-center text-xs text-muted-foreground">从右侧选择参考图或连接图片节点</div>
                  )}
                </div>
              ) : null}
              <div className="relative min-h-0 flex-1 overflow-hidden rounded-md border bg-muted">
                {generationImageSrc ? (
                  <>
                    {/* eslint-disable-next-line @next/next/no-img-element */}
                    <img src={generationImageSrc} alt="生成结果" className="h-full w-full object-contain" />
                    <div className="nodrag absolute right-2 top-2 flex gap-1 opacity-0 transition-opacity group-hover:opacity-100">
                      <button
                        type="button"
                        className="flex h-7 w-7 items-center justify-center rounded-md border bg-background/90 text-muted-foreground shadow-sm hover:text-foreground"
                        title="预览图片"
                        onPointerDown={(event) => event.stopPropagation()}
                        onClick={(event) => {
                          event.stopPropagation();
                          onPreviewNode(node.id);
                        }}
                      >
                        <Eye className="h-3.5 w-3.5" />
                      </button>
                      <button
                        type="button"
                        className="flex h-7 w-7 items-center justify-center rounded-md border bg-background/90 text-muted-foreground shadow-sm hover:text-foreground"
                        title="下载图片"
                        onPointerDown={(event) => event.stopPropagation()}
                        onClick={(event) => {
                          event.stopPropagation();
                          onDownloadNode(node.id);
                        }}
                      >
                        <Download className="h-3.5 w-3.5" />
                      </button>
                    </div>
                    <NodeOutputStrip node={node} onUpdateNode={onUpdateNode} />
                  </>
                ) : (
                  <div className="flex h-full items-center justify-center px-4 text-center text-sm text-muted-foreground">
                    {node.status === 'failed' ? node.error || '生成失败' : node.status === 'generating' ? '生成中...' : '等待节点内生成'}
                  </div>
                )}
              </div>
            </div>
          ) : null}

          {node.type === 'layeredImage' ? (
            <div className="grid h-full grid-cols-[1fr_120px] gap-3 rounded-md border bg-muted/20 p-3">
              <div className="relative overflow-hidden rounded-md border bg-background">
                <div className="absolute inset-0 bg-[linear-gradient(45deg,hsl(var(--muted))_25%,transparent_25%),linear-gradient(-45deg,hsl(var(--muted))_25%,transparent_25%),linear-gradient(45deg,transparent_75%,hsl(var(--muted))_75%),linear-gradient(-45deg,transparent_75%,hsl(var(--muted))_75%)] bg-[length:24px_24px] bg-[position:0_0,0_12px,12px_-12px,-12px_0px] opacity-35" />
                {(node.layers || []).filter(layer => layer.visible).map(layer => (
                  <div
                    key={layer.id}
                    className={cn(
                      'absolute border border-white/60 shadow-sm',
                      layer.type === 'text' ? 'flex items-center px-2 text-xs font-semibold' : 'rounded-md',
                      layer.locked ? 'outline outline-1 outline-dashed outline-muted-foreground/70' : '',
                    )}
                    style={{
                      left: `${(layer.x / layerCanvasSize) * 100}%`,
                      top: `${(layer.y / layerCanvasSize) * 100}%`,
                      width: `${(layer.width / layerCanvasSize) * 100}%`,
                      height: `${(layer.height / layerCanvasSize) * 100}%`,
                      backgroundColor: layer.type === 'text' ? 'transparent' : layer.color || layerColors[layer.type],
                      color: layer.color || layerColors.text,
                      opacity: layer.opacity ?? 1,
                    }}
                  >
                    {layer.type === 'text' ? layer.text || layer.name : null}
                  </div>
                ))}
              </div>
              <div className="min-w-0 space-y-1 overflow-y-auto">
                {(node.layers || []).slice().reverse().map(layer => (
                  <div key={layer.id} className="truncate rounded-md border bg-background px-2 py-1 text-[11px]">
                    <span className={layer.visible ? '' : 'text-muted-foreground line-through'}>{layer.name}</span>
                  </div>
                ))}
              </div>
            </div>
          ) : null}

          {node.type === 'frame' ? (
            <div className="flex h-full flex-col justify-end rounded-md border border-dashed bg-background/25 p-3 text-xs text-muted-foreground" style={{ borderColor: node.color || meta.accent }}>
              <div className="mb-auto inline-flex w-fit rounded-full px-2 py-1 text-[10px] font-semibold" style={{ color: meta.accent, backgroundColor: meta.bg }}>GROUP FRAME</div>
              <div className="rounded-md bg-background/90 px-3 py-2 shadow-sm">{node.text || '流程分组'}</div>
            </div>
          ) : null}
        </div>
      </div>
    </div>
  );
}

function sameFlowNodePositions(a: CanvasFlowNode[], b: CanvasFlowNode[]) {
  if (a.length !== b.length) return false;
  const bMap = new Map(b.map(node => [node.id, node]));
  return a.every(node => {
    const next = bMap.get(node.id);
    return !!next && node.position.x === next.position.x && node.position.y === next.position.y;
  });
}

function sameFlowNodes(a: CanvasFlowNode[], b: CanvasFlowNode[]) {
  if (a.length !== b.length) return false;
  const bMap = new Map(b.map(node => [node.id, node]));
  return a.every(node => {
    const next = bMap.get(node.id);
    return !!next
      && node.position.x === next.position.x
      && node.position.y === next.position.y
      && node.selected === next.selected
      && node.draggable === next.draggable
      && node.selectable === next.selectable
      && node.width === next.width
      && node.height === next.height
      && node.data.node === next.data.node
      && node.data.selected === next.data.selected
      && node.data.connecting === next.data.connecting
      && node.data.connections === next.data.connections
      && node.data.allNodes === next.data.allNodes
      && node.data.connectingFromId === next.data.connectingFromId
      && node.data.imageModelOptions === next.data.imageModelOptions
      && node.data.textModelOptions === next.data.textModelOptions
      && node.data.onRunNode === next.data.onRunNode
      && node.data.onRunDownstreamNode === next.data.onRunDownstreamNode
      && node.data.onCreateVariationNode === next.data.onCreateVariationNode
      && node.data.onPreviewNode === next.data.onPreviewNode
      && node.data.onDownloadNode === next.data.onDownloadNode;
  });
}

function clampViewport(viewport: FlowViewport, minZoom: number, maxZoom: number): CanvasViewport {
  return {
    x: viewport.x,
    y: viewport.y,
    zoom: Math.min(maxZoom, Math.max(minZoom, viewport.zoom)),
  };
}

function flowToScreen(point: { x: number; y: number }, viewport: CanvasViewport) {
  return {
    x: point.x * viewport.zoom + viewport.x,
    y: point.y * viewport.zoom + viewport.y,
  };
}

function buildConnectionPath(source: CanvasNode, target: CanvasNode, viewport: CanvasViewport) {
  const sourcePoint = flowToScreen({
    x: source.x + source.width + 8,
    y: source.y + source.height / 2,
  }, viewport);
  const targetPoint = flowToScreen({
    x: target.x - 8,
    y: target.y + target.height / 2,
  }, viewport);
  const sourceX = sourcePoint.x;
  const sourceY = sourcePoint.y;
  const targetX = targetPoint.x;
  const targetY = targetPoint.y;
  const distance = Math.max(80, Math.abs(targetX - sourceX) * 0.45);
  return `M ${sourceX} ${sourceY} C ${sourceX + distance} ${sourceY}, ${targetX - distance} ${targetY}, ${targetX} ${targetY}`;
}

function CanvasConnectionOverlay({
  nodes,
  connections,
  edgeStroke,
  edgeZIndex,
}: {
  nodes: CanvasNode[];
  connections: CanvasConnection[];
  edgeStroke: string;
  edgeZIndex: number;
}) {
  const viewport = useViewport();
  const nodeMap = useMemo(() => new Map(nodes.map(node => [node.id, node])), [nodes]);
  const visibleConnections = connections
    .map(connection => {
      const source = nodeMap.get(connection.sourceNodeId);
      const target = nodeMap.get(connection.targetNodeId);
      return source && target ? { connection, source, target } : null;
    })
    .filter((item): item is { connection: CanvasConnection; source: CanvasNode; target: CanvasNode } => !!item);

  if (visibleConnections.length === 0) return null;

  return (
    <svg
      className="canvas-connection-overlay"
      style={{
        position: 'absolute',
        inset: 0,
        width: '100%',
        height: '100%',
        overflow: 'visible',
        pointerEvents: 'none',
        zIndex: edgeZIndex,
      }}
      aria-hidden="true"
    >
      {visibleConnections.map(({ connection, source, target }) => (
        <path
          key={connection.id}
          d={buildConnectionPath(source, target, viewport)}
          fill="none"
          stroke={edgeStroke}
          strokeWidth={2}
          strokeLinecap="round"
          strokeLinejoin="round"
          opacity={0.72}
        />
      ))}
    </svg>
  );
}

function FlowCanvasInner({
  nodes,
  connections,
  imageModelOptions,
  textModelOptions,
  viewport,
  selectedNodeIds,
  connectingFromId,
  editable,
  minZoom,
  maxZoom,
  layerCanvasSize,
  layerColors,
  onSelectNode,
  onSelectionChange,
  onStartConnect,
  onUpdateNode,
  onRunNode,
  onRunDownstreamNode,
  onCreateVariationNode,
  onPreviewNode,
  onDownloadNode,
  onConnect,
  onRemoveConnection,
  onRemoveConnections,
  onNodesCommit,
  onViewportCommit,
  onReady,
  onPaneClick,
  onPaneDoubleClick,
}: ReactFlowCanvasProps) {
  const reactFlow = useReactFlow<CanvasFlowNode, Edge>();
  const { resolvedTheme } = useTheme();
  const draggingRef = useRef(false);
  const movingRef = useRef(false);
  const paneClickTimerRef = useRef<number | null>(null);
  const isDarkTheme = resolvedTheme === 'dark';
  const edgeStroke = isDarkTheme ? 'rgba(255, 255, 255, 0.52)' : 'rgba(126, 76, 28, 0.56)';
  const dotColor = isDarkTheme ? 'rgb(255 255 255 / 0.42)' : 'hsl(var(--foreground) / 0.16)';
  const dotAccentColor = isDarkTheme ? 'rgb(255 255 255 / 0.24)' : 'hsl(var(--primary) / 0.28)';

  const incomingNodes = useMemo<CanvasFlowNode[]>(() => nodes.map(node => ({
    id: node.id,
    type: 'canvasNode',
    position: { x: node.x, y: node.y },
    width: node.width,
    height: node.height,
    selected: selectedNodeIds.includes(node.id),
    draggable: editable,
    selectable: editable,
    zIndex: node.type === 'frame' ? Math.min(node.zIndex, 0) : node.zIndex,
    data: {
      node,
      selected: selectedNodeIds.includes(node.id),
      connecting: connectingFromId === node.id,
      connections,
      allNodes: nodes,
      connectingFromId,
      imageModelOptions,
      textModelOptions,
      layerCanvasSize,
      layerColors,
      onSelect: onSelectNode,
      onConnect,
      onStartConnect,
      onUpdateNode,
      onRunNode,
      onRunDownstreamNode,
      onCreateVariationNode,
      onPreviewNode,
      onDownloadNode,
      onRemoveConnection,
    },
    style: {
      width: node.width,
      height: node.height,
    },
  })), [connectingFromId, connections, editable, imageModelOptions, layerCanvasSize, layerColors, nodes, onConnect, onCreateVariationNode, onDownloadNode, onPreviewNode, onRemoveConnection, onRunDownstreamNode, onRunNode, onSelectNode, onStartConnect, onUpdateNode, selectedNodeIds, textModelOptions]);

  const [flowNodes, setFlowNodes] = useState<CanvasFlowNode[]>(incomingNodes);

  useEffect(() => {
    setFlowNodes(current => {
      if (draggingRef.current && sameFlowNodePositions(current, incomingNodes)) return current;
      if (sameFlowNodes(current, incomingNodes)) return current;
      return incomingNodes;
    });
  }, [incomingNodes]);

  useEffect(() => () => {
    if (paneClickTimerRef.current !== null) {
      window.clearTimeout(paneClickTimerRef.current);
    }
  }, []);

  useEffect(() => () => {
    onReady?.(null);
  }, [onReady]);

  const commitCurrentNodePositions = useCallback((options?: { history?: boolean; dirty?: boolean }) => {
    const positions = reactFlow.getNodes().map(node => ({
      id: node.id,
      x: node.position.x,
      y: node.position.y,
    }));
    onNodesCommit(positions, options);
  }, [onNodesCommit, reactFlow]);

  const handleNodesChange = useCallback((changes: NodeChange<CanvasFlowNode>[]) => {
    const relevantChanges = changes.filter(change => change.type === 'position' || change.type === 'select' || change.type === 'remove');
    if (relevantChanges.length === 0) return;

    setFlowNodes(current => {
      const removedIds = new Set(
        relevantChanges
          .filter((change): change is Extract<NodeChange<CanvasFlowNode>, { type: 'remove' }> => change.type === 'remove')
          .map(change => change.id),
      );
      let changed = removedIds.size > 0;
      const nextNodes = current.map(node => {
        if (removedIds.has(node.id)) return node;
        const positionChange = relevantChanges.find(
          (change): change is Extract<NodeChange<CanvasFlowNode>, { type: 'position' }> => change.type === 'position' && change.id === node.id && !!change.position,
        );
        const selectChange = relevantChanges.find(
          (change): change is Extract<NodeChange<CanvasFlowNode>, { type: 'select' }> => change.type === 'select' && change.id === node.id,
        );
        const nextPosition = positionChange?.position || node.position;
        const nextSelected = typeof selectChange?.selected === 'boolean' ? selectChange.selected : node.selected;
        if (nextPosition === node.position && nextSelected === node.selected) return node;
        changed = true;
        return {
          ...node,
          position: nextPosition,
          selected: nextSelected,
        };
      }).filter(node => !removedIds.has(node.id));
      return changed ? nextNodes : current;
    });

    const selected = relevantChanges
      .filter((change): change is Extract<NodeChange<CanvasFlowNode>, { type: 'select' }> => change.type === 'select' && change.selected)
      .map(change => change.id);
    if (selected.length > 0) onSelectionChange(selected);
  }, [onSelectionChange]);

  const handleEdgesChange = useCallback((changes: EdgeChange<Edge>[]) => {
    const removedIds = changes.filter(change => change.type === 'remove').map(change => change.id);
    if (removedIds.length > 0) onRemoveConnections(removedIds);
  }, [onRemoveConnections]);

  const handleConnect = useCallback((connection: Connection) => {
    if (!connection.source || !connection.target) return;
    onConnect(connection.source, connection.target);
  }, [onConnect]);

  const handleMoveEnd = useCallback((_event: MouseEvent | TouchEvent | null, nextViewport: FlowViewport) => {
    if (!movingRef.current) return;
    movingRef.current = false;
    const committedViewport = clampViewport(nextViewport, minZoom, maxZoom);
    onViewportCommit(committedViewport);
  }, [maxZoom, minZoom, onViewportCommit]);

  const handleNodeDragStart = useCallback(() => {
    draggingRef.current = true;
  }, []);

  const handleNodeDragStop = useCallback(() => {
    draggingRef.current = false;
    commitCurrentNodePositions({ history: true, dirty: true });
  }, [commitCurrentNodePositions]);

  const handleMoveStart = useCallback(() => {
    movingRef.current = true;
  }, []);

  const handlePaneReactFlowClick = useCallback((event: React.MouseEvent) => {
    const point = reactFlow.screenToFlowPosition({ x: event.clientX, y: event.clientY });
    if (event.detail >= 2) {
      if (paneClickTimerRef.current !== null) {
        window.clearTimeout(paneClickTimerRef.current);
        paneClickTimerRef.current = null;
      }
      onPaneDoubleClick(reactFlow.screenToFlowPosition({ x: event.clientX, y: event.clientY }, { snapToGrid: true, snapGrid: SNAP_GRID }), event);
      return;
    }
    if (paneClickTimerRef.current !== null) {
      window.clearTimeout(paneClickTimerRef.current);
    }
    paneClickTimerRef.current = window.setTimeout(() => {
      paneClickTimerRef.current = null;
      onPaneClick(point, event);
    }, 180);
  }, [onPaneClick, onPaneDoubleClick, reactFlow]);

  const handleSelectionChange = useCallback(({ nodes: selectedNodes }: { nodes: CanvasFlowNode[] }) => {
    onSelectionChange(selectedNodes.map(node => node.id));
  }, [onSelectionChange]);

  const getMiniMapNodeColor = useCallback((node: FlowNode) => (
    selectedNodeIds.includes(node.id) ? 'hsl(var(--primary))' : 'hsl(var(--muted-foreground))'
  ), [selectedNodeIds]);

  const handleInit = useCallback((instance: ReactFlowInstance<CanvasFlowNode, Edge>) => {
    onReady?.({
      setViewport: (nextViewport) => instance.setViewport(nextViewport, { duration: 120 }),
      zoomTo: (zoom) => instance.zoomTo(Math.min(maxZoom, Math.max(minZoom, zoom)), { duration: 120 }),
      fitBounds: (bounds, options) => instance.fitBounds(bounds, options),
      getViewport: () => clampViewport(instance.getViewport(), minZoom, maxZoom),
    });
  }, [maxZoom, minZoom, onReady]);

  return (
    <div className="relative h-full w-full">
      <ReactFlow
        className="canvas-flow h-full w-full"
        nodes={flowNodes}
        edges={[]}
        nodeTypes={CANVAS_NODE_TYPES}
        minZoom={minZoom}
        maxZoom={maxZoom}
        snapToGrid
        snapGrid={SNAP_GRID}
        defaultViewport={viewport}
        zoomOnScroll
        zoomOnPinch
        panOnScroll={false}
        panOnDrag={[0, 1]}
        selectionOnDrag={false}
        multiSelectionKeyCode={MULTI_SELECTION_KEYS}
        deleteKeyCode={null}
        zoomOnDoubleClick={false}
        nodesDraggable={editable}
        nodesConnectable={editable}
        nodesFocusable={false}
        edgesFocusable={false}
        connectionLineType={ConnectionLineType.SmoothStep}
        fitViewOptions={FLOW_FIT_VIEW_OPTIONS}
        onNodesChange={handleNodesChange}
        onEdgesChange={handleEdgesChange}
        onConnect={handleConnect}
        onNodeDragStart={handleNodeDragStart}
        onNodeDragStop={handleNodeDragStop}
        onMoveStart={handleMoveStart}
        onMoveEnd={handleMoveEnd}
        onPaneClick={handlePaneReactFlowClick}
        onSelectionChange={handleSelectionChange}
        onInit={handleInit}
      >
        <Background gap={20} size={1.15} color={dotColor} />
        <Background gap={100} size={1.8} color={dotAccentColor} />
        <style jsx global>{`
          .canvas-flow .react-flow__connection-path {
            stroke: ${edgeStroke};
            stroke-width: 2px;
            opacity: 1;
          }
        `}</style>
        <Controls showInteractive={false} position="bottom-left" />
        <MiniMap
          position="bottom-right"
          pannable
          zoomable
          nodeColor={getMiniMapNodeColor}
          maskColor="hsl(var(--background) / 0.58)"
        />
      </ReactFlow>
      <CanvasConnectionOverlay
        nodes={nodes}
        connections={connections}
        edgeStroke={edgeStroke}
        edgeZIndex={50}
      />
    </div>
  );
}

export function ReactFlowCanvas(props: ReactFlowCanvasProps) {
  return (
    <ReactFlowProvider>
      <FlowCanvasInner {...props} />
    </ReactFlowProvider>
  );
}
