<template>
	<div
		class="h-full w-full"
		ref="workflowWrapperRef"
		@drop="onDrop"
		@contextmenu="handleWorkflowContextMenu"
	>
		<Workflow
			ref="workflowRef"
			:id="workflow?.id"
			:workflow="workflowWithExecutionState"
			:nodeMap="nodeMap"
			:autoFit="false"
			@click:node="handleSelectNode"
			@dblclick:node="handleNodeClick"
			@create:node="handleNodeCreate"
			@create:connection:end="onCreateConnection"
			@drag-and-drop="handleDrop"
			@run:node="handleRunNode"
			@update:nodes:position="handleUpdateNodesPosition"
			@update:node:attrs="handleUpdateNodeProps"
			@delete:node="handleDeleteNode"
			@delete:connection="handleDeleteEdge"
			@dragover="onDragOver"
			@dragleave="onDragLeave"
			@create:connection:cancelled="onConnectionOpenNodeLibary"
			@click:connection:add="handleClickConectionAdd"
			@viewport:change="handleViewportChange"
			class="bg-#f5f5f5"
		>
			<Toolbar
				@create:node="handleNodeCreate"
				@run="handleRunAgent"
				:env-vars="workflow?.env_variables || []"
				:run-nodes="toolbarRunNodes"
				@change-env-vars="handleChangeEnvVars"
			/>
		</Workflow>
	</div>
	<RunWorkflow
		v-model:visible="runVisible"
		:workflow="workflow"
		:close-on-run="closeRunWorkflowOnSubmit"
		:input-only="runWorkflowInputOnly"
		:active-node-id="runWorkflowNodeId"
		:initial-tab="runWorkflowInitialTab"
		@run-started="handleWorkflowRunStarted"
	/>
	<Setter
		:id="nodeID"
		:workflow="workflow"
		:active-tab="setterActiveTab"
		@update:node:data="handleUpdateNode"
		@run-node="handleRunNode"
		v-model:visible="setterVisible"
	/>
	<el-popover
		v-if="libaryRefferenceRef"
		:visible="showNodeLibary"
		trigger="manual"
		:show-arrow="false"
		:append-to="workflowWrapperRef"
		:virtual-ref="libaryRefferenceRef"
		width="360px"
		virtual-triggering
	>
		<div ref="nodeLibraryPanelRef">
			<NodeLibary
				@add-node="handleNodeCreateFromLibrary"
				:parent-node-type="nodeLibaryParentType"
				hide-start
				ignore-drag
			/>
		</div>
	</el-popover>
	<Teleport to="body">
		<div
			v-if="contextMenuVisible"
			ref="contextMenuRef"
			class="workflow-context-menu"
			:style="contextMenuStyle"
			@contextmenu.prevent
		>
			<button type="button" class="workflow-context-menu__item" @click="handleContextAddNode">
				<Icon icon="lucide:plus" class="workflow-context-menu__icon">+</Icon>
				<span>添加节点</span>
			</button>
			<button type="button" class="workflow-context-menu__item" @click="handleContextAddStickyNote">
				<Icon icon="lucide:file-plus-corner" class="workflow-context-menu__icon"></Icon>
				<span>添加注释</span>
			</button>
			<button
				type="button"
				class="workflow-context-menu__item"
				:class="{ 'is-disabled': !selectedNodeId }"
				@click="handleContextRunNode"
			>
				<Icon icon="lucide:play" class="workflow-context-menu__icon"></Icon>
				<span>测试运行</span>
			</button>
		</div>
	</Teleport>
</template>

<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { agent } from '@repo/api-service'

import RunWorkflow from '@/features/RunWorkflow/index.vue'
import NodeLibary from '@/features/nodeLibary/index.vue'
import Toolbar from '@/features/toolbar/index.vue'
import Setter from '@/features/setter/index.vue'
import { nodeMap } from '@/nodes'
import { getNodeDisplayName } from '@/nodes/i18n'
import { useI18n } from '@/composables/useI18n'
import { useRunnerStore } from '@/store/modules/runner.store'
import { Workflow, useDragAndDrop } from '@repo/workflow'
import { useDebounceFn } from '@vueuse/core'
import { isEqual } from 'lodash-es'
import { Icon } from '@repo/ui'

import type {
	CanvasExecutionStatus,
	Connection,
	ConnectStartEvent,
	IWorkflow,
	IWorkflowNode,
	XYPosition
} from '@repo/workflow'

interface Props {
	workflow: IWorkflow
	reloadWorkflow: (agentId: string) => Promise<void>
}

const props = defineProps<Props>()
const { t } = useI18n()

// 将运行器状态映射为画布展示用的状态值。
const mapNodeExecutionStatus = (status?: string): CanvasExecutionStatus => {
	if (status === 'running') {
		return 'running'
	}

	if (status === 'success') {
		return 'success'
	}

	if (status === 'failed') {
		return 'warning'
	}

	if (status === 'suspended') {
		return 'suspended'
	}

	return 'idle'
}

const MIN_NODE_RUNNING_EFFECT_MS = 500
const runnerStore = useRunnerStore()
const workflowWrapperRef = ref<HTMLElement>()
const workflowRef = ref<InstanceType<typeof Workflow>>()
const showNodeLibary = ref(false)
const libaryRefferenceRef = ref<HTMLElement>()
const nodeLibaryPopoverAnchorRef = ref<HTMLElement>()
const nodeLibraryPanelRef = ref<HTMLElement>()
const contextMenuRef = ref<HTMLElement>()
const contextMenuVisible = ref(false)
const contextMenuPosition = ref({ x: 0, y: 0 })
const contextMenuFlowPosition = ref<XYPosition>({ x: 0, y: 0 })
const runVisible = ref(false)
const closeRunWorkflowOnSubmit = ref(false)
const runWorkflowInputOnly = ref(false)
const runWorkflowNodeId = ref('')
const runWorkflowInitialTab = ref<'input' | 'trigger' | 'result' | 'detail' | 'trace'>('input')
const setterVisible = ref(false)
const nodeID = ref('')
const setterActiveTab = ref<'setting' | 'last-run'>('setting')
const displayNodeExecutionStatus = ref<Record<string, CanvasExecutionStatus>>({})
const shouldAutoCenterAfterInit = ref(true)
const peddingHandlePayload = ref<{
	by?: 'node' | 'edge'
	handle?: ConnectStartEvent
	position: XYPosition
	event?: MouseEvent
	parentId?: string
	connection?: Connection & { id: string }
}>()

const runningStatusStartedAt = new Map<string, number>()
const pendingNodeStatusTimers = new Map<string, number>()
let hideNodeLibaryTimer: number | undefined
const pendingEdges = ref<
	Array<Connection & { id: string; type?: string; data?: Record<string, unknown> }>
>([])
const pendingNodes = ref<IWorkflowNode[]>([])

const selectedNodeId = computed(() => {
	const selectedNode = props.workflow?.nodes?.find((node) => node.selected)
	return selectedNode?.id || ''
})

const contextMenuStyle = computed(() => ({
	left: `${contextMenuPosition.value.x}px`,
	top: `${contextMenuPosition.value.y}px`
}))

const removeNodeLibaryPopoverAnchor = () => {
	nodeLibaryPopoverAnchorRef.value?.remove()
	nodeLibaryPopoverAnchorRef.value = undefined
}

const createNodeLibaryPopoverAnchor = (position?: { x: number; y: number }) => {
	removeNodeLibaryPopoverAnchor()

	if (!position) {
		return undefined
	}

	const anchor = document.createElement('div')
	anchor.style.position = 'fixed'
	anchor.style.left = `${position.x}px`
	anchor.style.top = `${position.y}px`
	anchor.style.width = '1px'
	anchor.style.height = '1px'
	anchor.style.pointerEvents = 'none'
	document.body.appendChild(anchor)
	nodeLibaryPopoverAnchorRef.value = anchor

	return anchor
}

const closeContextMenu = () => {
	contextMenuVisible.value = false
}

const getFlowPositionFromMouseEvent = (event: MouseEvent): XYPosition => {
	const screenToFlowCoordinate = workflowRef.value?.getVueFlow()?.screenToFlowCoordinate
	if (screenToFlowCoordinate) {
		return screenToFlowCoordinate({
			x: event.clientX,
			y: event.clientY
		})
	}

	const viewport = workflowRef.value?.getVueFlow()?.viewport?.value
	const bounds = workflowWrapperRef.value?.getBoundingClientRect()
	if (viewport && bounds) {
		return {
			x: (event.clientX - bounds.left - viewport.x) / viewport.zoom,
			y: (event.clientY - bounds.top - viewport.y) / viewport.zoom
		}
	}

	return { x: 0, y: 0 }
}

// 在为节点安排新的状态切换前，先清理旧的延时任务。
const clearPendingNodeStatusTimer = (nodeId: string) => {
	const timer = pendingNodeStatusTimers.get(nodeId)
	if (timer) {
		window.clearTimeout(timer)
		pendingNodeStatusTimers.delete(nodeId)
	}
}

// 将节点执行状态写入展示缓存，空闲状态则直接清理。
const applyDisplayedNodeStatus = (nodeId: string, status: CanvasExecutionStatus) => {
	if (status === 'idle') {
		delete displayNodeExecutionStatus.value[nodeId]
		return
	}

	displayNodeExecutionStatus.value[nodeId] = status
}

// 保证 running 态至少展示一小段时间，避免状态闪烁。
const syncDisplayedNodeStatus = (nodeId: string, nextStatus: CanvasExecutionStatus) => {
	const currentStatus = displayNodeExecutionStatus.value[nodeId] || 'idle'

	if (nextStatus === 'running') {
		clearPendingNodeStatusTimer(nodeId)
		runningStatusStartedAt.set(nodeId, Date.now())
		applyDisplayedNodeStatus(nodeId, 'running')
		return
	}

	const runningStartedAt = runningStatusStartedAt.get(nodeId)
	const shouldKeepRunning =
		currentStatus === 'running' &&
		typeof runningStartedAt === 'number' &&
		Date.now() - runningStartedAt < MIN_NODE_RUNNING_EFFECT_MS

	if (shouldKeepRunning) {
		clearPendingNodeStatusTimer(nodeId)
		const delay = MIN_NODE_RUNNING_EFFECT_MS - (Date.now() - runningStartedAt)
		const timer = window.setTimeout(() => {
			applyDisplayedNodeStatus(nodeId, nextStatus)
			runningStatusStartedAt.delete(nodeId)
			pendingNodeStatusTimers.delete(nodeId)
		}, delay)
		pendingNodeStatusTimers.set(nodeId, timer)
		return
	}

	clearPendingNodeStatusTimer(nodeId)
	applyDisplayedNodeStatus(nodeId, nextStatus)
	runningStatusStartedAt.delete(nodeId)
}

// 在运行器切换或组件卸载时重置所有执行态缓存。
const resetDisplayedNodeStatuses = () => {
	pendingNodeStatusTimers.forEach((timer) => window.clearTimeout(timer))
	pendingNodeStatusTimers.clear()
	runningStatusStartedAt.clear()
	displayNodeExecutionStatus.value = {}
}

watch(
	() => runnerStore.currentRunnerKey,
	() => {
		resetDisplayedNodeStatuses()
	}
)

watch(
	() => runnerStore.nodes.map((item) => ({ nodeId: item.nodeId, status: item.status })),
	(nodeStates) => {
		const activeNodeIds = new Set(nodeStates.map((item) => item.nodeId))

		nodeStates.forEach((item) => {
			syncDisplayedNodeStatus(item.nodeId, mapNodeExecutionStatus(item.status))
		})

		Object.keys(displayNodeExecutionStatus.value).forEach((nodeId) => {
			if (!activeNodeIds.has(nodeId)) {
				syncDisplayedNodeStatus(nodeId, 'idle')
			}
		})
	},
	{ immediate: true, deep: true }
)

// 将运行时执行状态合并到当前工作流数据，再交给画布渲染。
const workflowWithExecutionState = computed(() => {
	const baseWorkflow = props.workflow
	const nodes: IWorkflow['nodes'] = (baseWorkflow.nodes || []).map((node): IWorkflowNode => {
		const executionStatus: CanvasExecutionStatus =
			displayNodeExecutionStatus.value[node.id] || 'idle'

		return {
			...node,
			executionStatus,
			data: {
				...(node.data || {}),
				executionStatus
			}
		}
	})
	const stableNodeIds = new Set((baseWorkflow.nodes || []).map((node) => node.id))
	const displayPendingNodes = pendingNodes.value.filter((node) => !stableNodeIds.has(node.id))

	const stableEdgeKeys = new Set(
		(baseWorkflow.edges || []).map(
			(edge) => `${edge.source}->${edge.target}->${edge.sourceHandle || ''}`
		)
	)
	const displayPendingEdges = pendingEdges.value.filter(
		(edge) => !stableEdgeKeys.has(`${edge.source}->${edge.target}->${edge.sourceHandle || ''}`)
	)

	return {
		...baseWorkflow,
		nodes: [...nodes, ...displayPendingNodes],
		edges: [...(baseWorkflow.edges || []), ...displayPendingEdges]
	} as IWorkflow
})

const getNodeTypeById = (id?: string) => {
	if (!id) {
		return ''
	}

	const node = props.workflow?.nodes?.find((item) => item.id === id)
	return (node as any)?.nodeType || (node as any)?.data?.nodeType || ''
}

const nodeLibaryParentType = computed(() => getNodeTypeById(peddingHandlePayload.value?.parentId))

const toolbarRunNodes = computed(() => {
	return (props.workflow?.nodes || [])
		.filter((node) => {
			const nodeType = (node as any)?.nodeType || (node as any)?.data?.nodeType
			return ['start', 'trigger-schedule', 'trigger-webhook'].includes(nodeType || '')
		})
		.map((node) => {
			const nodeType = ((node as any)?.nodeType || (node as any)?.data?.nodeType || '') as string
			return {
				id: node.id,
				name: node.name || getNodeDisplayName(nodeType) || nodeType,
				nodeType
			}
		})
})

const { onDragOver, onDrop, onDragLeave } = useDragAndDrop({
	id: props.workflow.id,
	addNodes: (node) => {
		handleNodeCreate(node)
	}
})

// 统一处理画布内节点相关接口的成功与失败提示。
const handleApiResult = (response: any, successMessage?: string, errorMessage?: string) => {
	if (response?.isSuccess) {
		if (successMessage) {
			ElMessage.success(successMessage)
		}
		return true
	}
	if (response?.code === 0 && response?.error) {
		ElMessage.error(response.error)
		return false
	}
	if (errorMessage) {
		ElMessage.error(errorMessage)
	}
	return false
}

// 在提交节点更新前，整理出后端需要的标准节点结构。
const buildUpdateNodePayload = (node: any) => {
	return {
		...node,
		appAgentId: props.workflow.id,
		parentId: node.parentId || node.data?.parentId || '',
		position: node.position || { x: 20, y: 30 },
		width: node.width ?? node.data?.width ?? 96,
		height: node.height ?? node.data?.height ?? 96,
		selected: !!node.selected,
		nodeType: node.data?.nodeType || node.nodeType,
		zIndex: node.zIndex ?? 1
	}
}

// 将跨循环边重新绑定到可见的父节点上，保证连线位置正确。
const normalizeEdgeEndpoints = (
	edge: { source: string; target: string },
	nodes: Array<{ id: string; parentId?: string }>
) => {
	const sourceNode = nodes.find((node) => node.id === edge.source)
	const targetNode = nodes.find((node) => node.id === edge.target)

	const sourceParentId = sourceNode?.parentId || ''
	const targetParentId = targetNode?.parentId || ''

	if (sourceParentId && sourceParentId !== targetParentId) {
		return {
			source: sourceParentId,
			target: edge.target
		}
	}

	if (targetParentId && targetParentId !== sourceParentId) {
		return {
			source: edge.source,
			target: targetParentId
		}
	}

	return edge
}

// 新建连线时复用同一套端点归一化逻辑。
const normalizeConnectionEndpoints = (connection: Connection) => {
	return normalizeEdgeEndpoints(connection, props.workflow?.nodes || [])
}

const openSetter = (id: string, tab: 'setting' | 'last-run' = 'setting') => {
	nodeID.value = id
	setterActiveTab.value = tab
	setterVisible.value = true
}

const centerWorkflowIntoView = async () => {
	const vueFlow = workflowRef.value?.getVueFlow()
	if (!vueFlow || !props.workflow?.nodes?.length) {
		return false
	}

	await nextTick()

	await new Promise<void>((resolve) => {
		window.requestAnimationFrame(() => resolve())
	})

	await vueFlow.fitView({
		padding: 0.2,
		duration: 240,
		maxZoom: 1
	})

	return true
}

const openRunWorkflow = async (
	nodeId: string,
	initialTab: 'input' | 'trigger' | 'result' | 'detail' | 'trace',
	options?: {
		closeOnRun?: boolean
		inputOnly?: boolean
	}
) => {
	runWorkflowNodeId.value = nodeId
	runWorkflowInitialTab.value = initialTab
	closeRunWorkflowOnSubmit.value = !!options?.closeOnRun
	runWorkflowInputOnly.value = !!options?.inputOnly
	runVisible.value = false
	await nextTick()
	runVisible.value = true
}

// 从节点级操作入口直接运行指定节点。
const handleRunNode = async (id: string) => {
	if (!props.workflow?.id) {
		ElMessage.warning(t('pages.nodeView.messages.selectNodeFirst'))
		return
	}

	const targetNode = props.workflow?.nodes?.find((node) => node.id === id)
	const nodeType = (targetNode as any)?.nodeType || (targetNode as any)?.data?.nodeType

	if (nodeType === 'start') {
		await openRunWorkflow(id, 'input', {
			closeOnRun: true,
			inputOnly: true
		})
		return
	}

	if (['trigger-schedule', 'trigger-webhook'].includes(nodeType || '')) {
		try {
			const response = await agent.postAgentDoExecute({
				appAgentId: props.workflow.id,
				start_node_id: id,
				is_debugger: true,
				responseType: 'ws',
				params: {}
			})
			const agentRunnerKey = response?.result
			if (agentRunnerKey) {
				runnerStore.startRunner(agentRunnerKey, id)
				await openRunWorkflow(id, 'trigger')
				return
			}
			ElMessage.error(t('pages.nodeView.messages.runFailed'))
		} catch (error) {
			console.error('postDoTestNodeRunner error', error)
			ElMessage.error(t('pages.nodeView.messages.runFailed'))
		}
		return
	}

	closeRunWorkflowOnSubmit.value = false
	runWorkflowInputOnly.value = false

	try {
		const response = await agent.postAgentDoExecute({
			appAgentId: props.workflow.id,
			start_node_id: id,
			is_debugger: true,
			responseType: 'ws',
			// 如果是start用户输入则传入数据
			params: {}
		})
		const agentRunnerKey = response?.result
		if (agentRunnerKey) {
			runnerStore.startRunner(agentRunnerKey, id)
			openSetter(id, 'last-run')
		}
	} catch (error) {
		console.error('postDoTestNodeRunner error', error)
		ElMessage.error(t('pages.nodeView.messages.runFailed'))
	}
}

/**
 * 运行智能体
 */
const handleRunAgent = (id?: string) => {
	const targetNode =
		(id && props.workflow?.nodes?.find((node) => node.id === id)) ||
		(props.workflow?.nodes || []).find((node) => {
			const nodeType = (node as any)?.nodeType || (node as any)?.data?.nodeType
			return ['start', 'trigger-schedule', 'trigger-webhook'].includes(nodeType || '')
		})

	if (!props.workflow?.id || !targetNode?.id) {
		ElMessage.warning(t('pages.nodeView.messages.missingTrigger'))
		return
	}

	const nodeType = (targetNode as any)?.nodeType || (targetNode as any)?.data?.nodeType
	if (nodeType === 'start') {
		openRunWorkflow(targetNode.id, 'input')
		return
	}

	handleRunNode(targetNode.id)
}

const createStickyNoteNode = (position: XYPosition = { x: 600, y: 300 }) => {
	props.workflow?.nodes.push({
		appAgentId: props.workflow.id,
		type: 'canvas-node',
		zIndex: -1,
		nodeType: 'stickyNote',
		position,
		id: `stickyNote_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
		name: t('pages.nodeView.stickyNote.name'),
		remark: '',
		data: {
			id: '',
			version: ['1.0.0'],
			inputs: [],
			outputs: [],
			position,
			nodeType: 'stickyNote',
			content: t('pages.nodeView.stickyNote.content'),
			width: 400,
			height: 200,
			color: '#fff5d6'
		}
	})
}

watch(
	() => props.workflow?.nodes,
	async (nodes) => {
		if (!shouldAutoCenterAfterInit.value || !nodes?.length) {
			return
		}

		const centered = await centerWorkflowIntoView()
		if (centered) {
			shouldAutoCenterAfterInit.value = false
		}
	},
	{ flush: 'post' }
)

/**
 * 创建新节点
 */
const handleNodeCreate = (value: { type: string; position?: XYPosition } | string) => {
	if (typeof value === 'string') {
		if (value === 'stickyNote') {
			createStickyNoteNode()
		}
		return
	}

	if (value.type === 'stickyNote') {
		createStickyNoteNode(value.position)
		return
	}

	const nodeToAdd = nodeMap[value.type]?.schema
	const viewport = workflowRef.value?.getVueFlow()?.viewport
	const parentNodeType = getNodeTypeById(peddingHandlePayload.value?.parentId)
	const isLoopContainer = ['loop', 'iteration'].includes(parentNodeType)

	if (value.type === 'loop-end' && !isLoopContainer) {
		ElMessage.warning(t('pages.nodeView.messages.loopEndOnlyInside'))
		onHideNodeLibary()
		return
	}

	if (isLoopContainer && ['loop', 'iteration'].includes(value.type)) {
		ElMessage.warning(t('pages.nodeView.messages.noNestedLoop'))
		onHideNodeLibary()
		return
	}

	if (nodeToAdd) {
		const hasExplicitPosition = !!value.position
		const newNodeParam: any = {
			...nodeToAdd,
			appAgentId: props.workflow?.id || '',
			position: value.position
		}

		if (!hasExplicitPosition && !peddingHandlePayload.value && viewport) {
			newNodeParam.position = {
				x: (-viewport.value.x + window.innerWidth / 2) / viewport.value.zoom,
				y: (-viewport.value.y + window.innerHeight / 2) / viewport.value.zoom
			}
		}

		if (peddingHandlePayload.value) {
			const { position, handle, parentId } = peddingHandlePayload.value

			newNodeParam.position = position
			newNodeParam.prevNodeId = handle?.nodeId
			newNodeParam.parentId = parentId
			// 包含下划线/纯UUID的需要传值
			if (
				handle?.handleId?.includes('_') ||
				(handle?.handleId && !handle.handleId.includes('-source'))
			) {
				newNodeParam.nodeHandleId = handle?.handleId
			}
		}

		if (!newNodeParam.position) {
			newNodeParam.position = nodeToAdd.position
		}

		if (peddingHandlePayload.value?.by === 'edge' && peddingHandlePayload.value?.connection) {
			const { connection } = peddingHandlePayload.value
			const params = {
				edgeId: connection.id,
				newNode: newNodeParam
			}
			const pendingNodeId = `pending-node-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
			pendingNodes.value.push({
				...newNodeParam,
				id: pendingNodeId,
				name: 'pendding...',
				type: 'canvas-node',
				selected: false,
				data: {
					...(newNodeParam.data || {}),
					pending: true
				}
			} as IWorkflowNode)
			agent
				.postAgentDoNewAgentNodeWithEdge(params)
				.then(async (response) => {
					if (
						handleApiResult(
							response,
							t('pages.nodeView.messages.nodeAdded'),
							t('pages.nodeView.messages.addNodeFailed')
						)
					) {
						await props.reloadWorkflow(props.workflow.id)
					}
				})
				.catch((error) => {
					console.error('postAgentDoNewAgentNodeWithEdge error', error)
					ElMessage.error(t('pages.nodeView.messages.addNodeFailed'))
				})
				.finally(() => {
					pendingNodes.value = pendingNodes.value.filter((node) => node.id !== pendingNodeId)
				})
			onHideNodeLibary()
			return
		}

		onHideNodeLibary()

		const pendingNodeId = `pending-node-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
		pendingNodes.value.push({
			...newNodeParam,
			id: pendingNodeId,
			name: 'pendding...',
			type: 'canvas-node',
			selected: false,
			data: {
				...(newNodeParam.data || {}),
				pending: true
			}
		} as IWorkflowNode)

		agent
			.postAgentDoNewAgentNode(newNodeParam)
			.then(async (response) => {
				if (
					handleApiResult(
						response,
						t('pages.nodeView.messages.nodeAdded'),
						t('pages.nodeView.messages.addNodeFailed')
					)
				) {
					await props.reloadWorkflow(props.workflow.id)
				}
			})
			.catch((error) => {
				console.error('postDoNewAgentNode error', error)
				ElMessage.error(t('pages.nodeView.messages.addNodeFailed'))
			})
			.finally(() => {
				pendingNodes.value = pendingNodes.value.filter((node) => node.id !== pendingNodeId)
			})
	}
}

const handleNodeCreateFromLibrary = (value: { type: string; position?: XYPosition } | string) => {
	if (typeof value === 'string') {
		handleNodeCreate(value)
		return
	}

	handleNodeCreate({
		...value,
		position: value.position || contextMenuFlowPosition.value
	})
}

const openNodeLibraryAtContextMenu = async () => {
	await nextTick()
	if (hideNodeLibaryTimer) {
		window.clearTimeout(hideNodeLibaryTimer)
		hideNodeLibaryTimer = undefined
	}
	libaryRefferenceRef.value = createNodeLibaryPopoverAnchor(contextMenuPosition.value)
	peddingHandlePayload.value = {
		position: contextMenuFlowPosition.value
	}
	showNodeLibary.value = true
}

const handleContextAddNode = () => {
	closeContextMenu()
	void openNodeLibraryAtContextMenu()
}

const handleContextAddStickyNote = () => {
	closeContextMenu()
	handleNodeCreate({
		type: 'stickyNote',
		position: contextMenuFlowPosition.value
	})
}

const handleContextRunNode = () => {
	if (!selectedNodeId.value) {
		ElMessage.warning(t('pages.nodeView.messages.selectNodeFirst'))
		closeContextMenu()
		return
	}

	closeContextMenu()
	void handleRunNode(selectedNodeId.value)
}

const handleWorkflowContextMenu = (event: MouseEvent) => {
	const target = event.target as HTMLElement | null
	if (target?.closest('.workflow-context-menu')) {
		return
	}

	const wrapperEl = workflowWrapperRef.value
	if (!wrapperEl?.contains(target)) {
		return
	}

	event.preventDefault()
	onHideNodeLibary()

	contextMenuFlowPosition.value = getFlowPositionFromMouseEvent(event)
	contextMenuPosition.value = {
		x: event.clientX,
		y: event.clientY
	}
	contextMenuVisible.value = true
}

// 双击节点时打开对应的配置面板。
const handleNodeClick = (id: string, _position: XYPosition) => {
	openSetter(id, 'setting')
}

const handleWorkflowRunStarted = (id: string) => {
	if (!closeRunWorkflowOnSubmit.value) {
		return
	}

	openSetter(id, 'last-run')
}

// 将拖拽落点转换成统一的节点创建流程。
const handleDrop = (position: XYPosition, event: DragEvent) => {
	const type = event.dataTransfer?.getData('application/x-node-type')
	if (!type) return

	handleNodeCreate({ type, position })
}

// 持久化新建连线；若相同起终点已存在，则不重复创建。
const onCreateConnection = async (connection: Connection) => {
	const { sourceHandle } = connection
	const { source, target } = normalizeConnectionEndpoints(connection)
	const edgeKey = `${source}->${target}->${sourceHandle || ''}`

	const params: {
		appAgentId: string
		source: string
		target: string
		zIndex: number
		sourceHandle?: string
	} = {
		appAgentId: props.workflow.id,
		source,
		target,
		zIndex: 1
	}
	// 包含下划线或者无-source的需要传值
	if (
		(sourceHandle && sourceHandle.includes('_')) ||
		(sourceHandle && !sourceHandle.includes('-source'))
	) {
		params.sourceHandle = sourceHandle
	}

	const existsInWorkflow = props.workflow?.edges.some(
		(edge) => `${edge.source}->${edge.target}->${edge.sourceHandle || ''}` === edgeKey
	)
	const existsInPending = pendingEdges.value.some(
		(edge) => `${edge.source}->${edge.target}->${edge.sourceHandle || ''}` === edgeKey
	)
	if (existsInWorkflow || existsInPending) {
		return
	}

	const pendingId = `pending-edge-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
	pendingEdges.value.push({
		id: pendingId,
		type: 'canvas-edge',
		source,
		target,
		sourceHandle: params.sourceHandle,
		targetHandle: connection.targetHandle,
		data: {
			pending: true
		}
	})

	try {
		const response = await agent.postAgentDoNewEdge(params)

		if (
			handleApiResult(
				response,
				t('pages.nodeView.messages.edgeCreated'),
				t('pages.nodeView.messages.createEdgeFailed')
			)
		) {
			await props.reloadWorkflow(props.workflow.id)
		}
	} catch (error) {
		console.error('postAgentDoNewEdge error', error)
		ElMessage.error(t('pages.nodeView.messages.createEdgeFailed'))
	} finally {
		pendingEdges.value = pendingEdges.value.filter((edge) => edge.id !== pendingId)
	}
}

// 在拖拽结束后持久化节点坐标变化。
const handleUpdateNodesPosition = (events: { id: string; position: XYPosition }[]) => {
	events?.forEach(({ id, position }) => {
		const node = props.workflow?.nodes.find((item) => item.id === id)
		if (node) {
			if (node.position?.x === position.x && node.position?.y === position.y) {
				return
			}
			node.position = position
			agent
				.postAgentDoUpdateAgentNode(buildUpdateNodePayload(node))
				.then((response) => {
					handleApiResult(response, undefined, t('pages.nodeView.messages.updateNodeFailed'))
				})
				.catch((error) => {
					console.error('postDoUpdateAgentNode error', error)
				})
		}
	})
}

// 保持本地工作流中同一时间仅有一个节点处于选中状态。
const handleSelectNode = (id: string) => {
	props.workflow?.nodes.forEach((node) => {
		node.selected = false
	})
	const node = props.workflow?.nodes.find((item) => item.id === id)
	if (node) {
		node.selected = true
	}
}

// 持久化配置面板提交的节点数据变更。
const handleUpdateNode = (node: IWorkflowNode) => {
	if (node) {
		// 根据节点配置的 inputs/outputs 数量动态调整高度（超过 2 个端口时增加高度）
		const schema = nodeMap[node.nodeType]
		if (schema) {
			const data = node.data || {}

			const inputs =
				typeof schema.inputs === 'function' ? schema.inputs(data as any) : schema.inputs || []
			const outputs =
				typeof schema.outputs === 'function' ? schema.outputs(data as any) : schema.outputs || []

			const maxPorts = Math.max(inputs.length || 0, outputs.length || 0)
			// todo: 特殊情况处理
			if (maxPorts > 2) {
				const extraPorts = maxPorts - 2
				const baseHeight = 96
				const perPortOffset = 32
				node.height = baseHeight + extraPorts * perPortOffset
			}
		}

		const workflowNode = props.workflow?.nodes.find((item) => item.id === node.id)
		const syncedNode = workflowNode || node

		if (workflowNode) {
			const mergedData = {
				...(workflowNode.data || {}),
				...(node.data || {})
			}

			Object.assign(workflowNode, node)
			workflowNode.data = mergedData
		}
		agent
			.postAgentDoUpdateAgentNode(buildUpdateNodePayload(syncedNode))
			.then((response: any) => {
				if (!handleApiResult(response, undefined, t('pages.nodeView.messages.updateNodeFailed'))) {
					return
				}

				const responseNode = response?.result
				if (!responseNode || typeof responseNode !== 'object') {
					return
				}

				const latestNode = props.workflow?.nodes.find(
					(item) => item.id === (responseNode.id || node.id)
				)
				if (!latestNode) {
					return
				}

				const mergedData = {
					...(latestNode.data || {}),
					...(responseNode.data || {})
				}

				Object.assign(latestNode, responseNode)
				latestNode.data = mergedData
			})
			.catch((error) => {
				console.error('postDoUpdateAgentNode error', error)
			})
	}
}

// 持久化画布自身抛出的轻量节点属性更新。
const handleUpdateNodeProps = (id: string, attrs: Record<string, unknown>) => {
	const node = props.workflow?.nodes.find((item) => item.id === id)
	if (node) {
		const keys = Object.keys(attrs || {})
		const meaningfulKeys = keys.filter((key) => !['selected', 'dragging'].includes(key))
		if (meaningfulKeys.length === 0) {
			return
		}
		if (node.data?.nodeType === 'stickyNote') {
			Object.assign(node.data, attrs)
		} else {
			Object.assign(node, attrs)
		}
		agent
			.postAgentDoUpdateAgentNode(buildUpdateNodePayload(node))
			.then((response) => {
				handleApiResult(response, undefined, t('pages.nodeView.messages.updateNodeFailed'))
			})
			.catch((error) => {
				console.error('postDoUpdateAgentNode error', error)
			})
	}
}

// 确认后删除节点，并重新加载工作流图数据。
const handleDeleteNode = async (id: string) => {
	const index = props.workflow.nodes.findIndex((node) => node.id === id)
	if (index !== -1) {
		ElMessageBox.confirm(t('common.confirmDelete.message'), t('common.confirmDelete.title'), {
			confirmButtonText: t('common.confirm'),
			cancelButtonText: t('common.cancel'),
			type: 'warning'
		}).then(async () => {
			await agent.postAgentDoDeleteAgentNode({
				id
			})
			await props.reloadWorkflow(props.workflow.id)
		})
	}
}

// 确认后删除连线，并重新加载工作流图数据。
const handleDeleteEdge = async (connection: Connection & { id: string }) => {
	if (connection.id) {
		ElMessageBox.confirm(t('common.confirmDelete.message'), t('common.confirmDelete.title'), {
			confirmButtonText: t('common.confirm'),
			cancelButtonText: t('common.cancel'),
			type: 'warning'
		}).then(async () => {
			await agent.postAgentDoDeleteEdge({
				id: connection.id
			})
			await props.reloadWorkflow(props.workflow.id)
		})
	}
}

// 保存工具栏里修改的环境变量，并刷新当前工作流。
const handleChangeEnvVars = async (
	envVars: {
		name: string
		value: string
		type: 'string' | 'number' | 'boolean' | 'object' | 'array'
	}[]
) => {
	const response = await agent.postAgentDoSaveAgentVariables({
		appAgentId: props.workflow.id,
		conversation_variables: [],
		env_variables: envVars
	})
	handleApiResult(
		response,
		t('pages.nodeView.messages.envSaved'),
		t('pages.nodeView.messages.saveEnvFailed')
	)
	await props.reloadWorkflow(props.workflow.id)
}

// 连线创建被取消时，打开节点库以继续补全流程。
const onConnectionOpenNodeLibary = async (payload: {
	handle: ConnectStartEvent
	position: XYPosition
	event: MouseEvent
	parentId?: string
}) => {
	await nextTick()
	libaryRefferenceRef.value = createNodeLibaryPopoverAnchor({
		x: payload.event.clientX,
		y: payload.event.clientY
	})
	showNodeLibary.value = true
	peddingHandlePayload.value = payload
}

// 关闭节点库弹层时，清理相关的临时状态。
const onHideNodeLibary = () => {
	showNodeLibary.value = false

	if (hideNodeLibaryTimer) {
		window.clearTimeout(hideNodeLibaryTimer)
	}

	hideNodeLibaryTimer = window.setTimeout(() => {
		peddingHandlePayload.value = undefined
		libaryRefferenceRef.value = undefined
		removeNodeLibaryPopoverAnchor()
		hideNodeLibaryTimer = undefined
	}, 500)
}

const onGlobalPointerDown = (event: PointerEvent) => {
	if (contextMenuVisible.value) {
		const target = event.target as Node | null
		if (!target || !contextMenuRef.value?.contains(target)) {
			closeContextMenu()
		}
	}

	if (!showNodeLibary.value) {
		return
	}

	const target = event.target as Node | null
	if (!target) {
		onHideNodeLibary()
		return
	}

	const panelEl = nodeLibraryPanelRef.value
	if (panelEl?.contains(target)) {
		return
	}

	const anchorEl = libaryRefferenceRef.value
	if (anchorEl?.contains(target)) {
		return
	}

	onHideNodeLibary()
}

const onGlobalKeyDown = (event: KeyboardEvent) => {
	if (event.key === 'Escape') {
		closeContextMenu()
		if (showNodeLibary.value) {
			onHideNodeLibary()
		}
	}
}

const onGlobalScroll = () => {
	closeContextMenu()
}

// 根据边上加号按钮的位置打开节点库弹层。
const handleClickConectionAdd = (connection: Connection & { id: string }, parentId?: string) => {
	const el = document.querySelector(`[edge-add-btn="${connection.id}"]`) as HTMLElement
	const screenToFlowCoordinate = workflowRef.value?.getVueFlow()?.screenToFlowCoordinate

	if (el && screenToFlowCoordinate) {
		const rect = el.getBoundingClientRect()
		const position = screenToFlowCoordinate({
			x: rect.left,
			y: rect.top
		})

		libaryRefferenceRef.value = createNodeLibaryPopoverAnchor({
			x: rect.left,
			y: rect.top
		})
		showNodeLibary.value = true
		peddingHandlePayload.value = {
			by: 'edge',
			position: parentId ? { x: 50, y: 50 } : position,
			connection,
			parentId
		}
	}
}

/**
 * 视图切换
 */
const handleViewportChange = useDebounceFn((viewport: { x: number; y: number; zoom: number }) => {
	if (!isEqual(viewport, props.workflow?.viewport)) {
		agent.postAgentDoEditAgent({
			data: {
				...props.workflow,
				viewPort: viewport
			}
		})
	}
}, 1000)

onBeforeUnmount(() => {
	if (hideNodeLibaryTimer) {
		window.clearTimeout(hideNodeLibaryTimer)
	}
	removeNodeLibaryPopoverAnchor()
	resetDisplayedNodeStatuses()
	document.removeEventListener('pointerdown', onGlobalPointerDown, true)
	document.removeEventListener('keydown', onGlobalKeyDown)
	window.removeEventListener('scroll', onGlobalScroll, true)
})

onMounted(() => {
	document.addEventListener('pointerdown', onGlobalPointerDown, true)
	document.addEventListener('keydown', onGlobalKeyDown)
	window.addEventListener('scroll', onGlobalScroll, true)
})
</script>

<style scoped lang="less">
.workflow-context-menu {
	position: fixed;
	z-index: 3000;
	min-width: 148px;
	padding: 6px;
	border: 1px solid #eaecf0;
	border-radius: 8px;
	background: #fff;
	box-shadow: 0 12px 28px rgba(16, 24, 40, 0.14);
}

.workflow-context-menu__item {
	display: flex;
	align-items: center;
	gap: 8px;
	width: 100%;
	height: 34px;
	padding: 0 10px;
	border: 0;
	border-radius: 6px;
	background: transparent;
	color: #344054;
	font-size: 13px;
	line-height: 1;
	text-align: left;
	cursor: pointer;
}

.workflow-context-menu__item:hover:not(.is-disabled) {
	background: #f2f4f7;
	color: #296dff;
}

.workflow-context-menu__item.is-disabled {
	color: #98a2b3;
	cursor: not-allowed;
}

.workflow-context-menu__icon {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 16px;
	height: 16px;
	font-size: 12px;
	font-weight: 700;
}
</style>
