<template>
	<div class="flow-editor" v-loading="loading">
		<!-- 顶部工具栏 -->
		<div class="flow-toolbar">
			<div class="flow-toolbar__left">
				<el-button v-if="!readonly" type="primary" icon="el-icon-plus" size="small" round @click="addStep">新增工序</el-button>
				<el-button icon="el-icon-magic-stick" size="small" round @click="relayout">自动布局</el-button>
				<el-button icon="el-icon-full-screen" size="small" round @click="fitToWidth">适应宽度</el-button>
				<el-button icon="el-icon-circle-check" size="small" round @click="doValidate" :loading="validating">校验</el-button>
			</div>
			<div class="flow-legend">
				<span class="lg lg--raw">原料</span>
				<span class="lg lg--mid">半成品</span>
				<span class="lg lg--final">成品</span>
			</div>
		</div>
		<div class="flow-tip" v-if="!readonly">
			从左侧拖入原料作为起点 → 节点间拉线新建工序，拉到空白处自动生成半成品，连到产品节点收尾；单击线条编辑，右键线条删工序 / 右键节点移除物料
		</div>
		<div class="flow-tip flow-tip--ro" v-else>当前状态不可编辑（仅创建中的工艺可编辑）</div>

		<div class="flow-body">
			<!-- 左侧物料面板：拖入画布作为节点 -->
			<div class="flow-palette" v-if="!readonly">
				<div class="palette-head">
					<div class="palette-title">物料库</div>
					<el-select v-model="matType" size="small" @change="loadMaterials" style="width: 100%">
						<el-option label="主要原料" value="30"></el-option>
						<el-option label="成品" value="10"></el-option>
						<el-option label="半成品" value="20"></el-option>
					</el-select>
					<el-input
						v-model="matKeyword"
						size="small"
						clearable
						prefix-icon="el-icon-search"
						placeholder="搜索物料"
						style="margin-top: 6px"
					></el-input>
				</div>
				<div class="palette-list" v-loading="matLoading">
					<div
						v-for="m in filteredMaterials"
						:key="m.id"
						class="palette-item"
						:draggable="true"
						@dragstart="onDragStart($event, m)"
						:title="m.full_name"
					>
						<i class="el-icon-rank palette-item__grip"></i>
						<span class="palette-item__txt">{{ m.full_name }}</span>
					</div>
					<div v-if="!matLoading && filteredMaterials.length === 0" class="palette-empty">无匹配物料</div>
				</div>
			</div>

			<!-- 画布 -->
			<div ref="canvas" class="flow-canvas" @drop="onDrop" @dragover.prevent>
				<VueFlow
					:id="flowId"
					:default-viewport="{ zoom: 0.9 }"
					:min-zoom="0.2"
					:max-zoom="2"
					:pan-on-scroll="true"
					:zoom-on-scroll="false"
					:connection-radius="8"
					:connect-on-click="false"
					:nodes-draggable="true"
					:nodes-connectable="!readonly"
					:elements-selectable="true"
					@nodes-initialized="onNodesInit"
					@connect-start="onConnectStart"
					@connect="onConnect"
					@connect-end="onConnectEnd"
					@edge-click="onEdgeClick"
					@edge-context-menu="onEdgeContextMenu"
					@node-context-menu="onNodeContextMenu"
				>
					<!-- 自定义物料节点（与 xtFlowViewer 共用） -->
					<template #node-material="{ data }">
						<material-node :data="data" />
					</template>

					<Background :gap="18" pattern-color="#e6e9ef" />
					<Controls :show-interactive="false" />
				</VueFlow>
				<div v-if="!loading && nodes.length === 0" class="flow-empty">
					<i class="el-icon-position flow-empty__icon"></i>
					<div>从左侧把「原料」拖到这里开始搭建工艺路线</div>
				</div>
			</div>
		</div>

		<!-- 复用现有单步表单：新增/编辑一条 Route；key 随每次打开递增强制重建实例，预填走 prop 在挂载时即确定 -->
		<route-form
			v-if="dialogVisible"
			:key="dialogSeq"
			ref="routeForm"
			:count="count"
			:routepack="routepack"
			:prefill="formPrefill"
			@success="onFormSuccess"
			@closed="dialogVisible = false"
		></route-form>
	</div>
</template>

<script>
import { VueFlow, useVueFlow } from "@vue-flow/core";
import { Background } from "@vue-flow/background";
import { Controls } from "@vue-flow/controls";
import routeForm from "@/views/mtm/route_form.vue";
import MaterialNode from "./flow/MaterialNode.vue";
import { layoutGraph, NODE_W } from "./flow/dagreLayout";
import "@vue-flow/core/dist/style.css";
import "@vue-flow/core/dist/theme-default.css";
import "@vue-flow/controls/dist/style.css";

// 每个编辑器实例分配唯一 flowId，避免 Vue Flow 全局 store 按固定 id 缓存导致跨次残留
let flowSeq = 0;

export default {
	name: "xtFlowEditor",
	components: { VueFlow, Background, Controls, routeForm, MaterialNode },
	props: {
		// 工艺包 id
		routepack: { type: String, default: "" },
		// 工艺包产品：常驻画布作为终点节点，可从其他节点拉线连入
		productId: { type: [String, Number], default: "" },
		productName: { type: String, default: "" },
		// 是否只读（非“创建中”状态）
		readonly: { type: Boolean, default: false },
	},
	emits: ["changed"],
	setup() {
		const flowId = "route-flow-editor-" + ++flowSeq;
		const { setViewport, screenToFlowCoordinate, project, setNodes, setEdges, addNodes, removeNodes } =
			useVueFlow(flowId);
		return { flowId, setViewport, screenToFlowCoordinate, project, setNodes, setEdges, addNodes, removeNodes };
	},
	data() {
		return {
			loading: false,
			validating: false,
			routes: [],
			nodes: [],
			edges: [],
			count: 0,
			dialogVisible: false,
			dialogSeq: 0,
			formPrefill: null,
			// 左侧物料面板
			matType: "30",
			matKeyword: "",
			materials: [],
			matLoading: false,
			// 客户端临时种子节点（尚未挂到任何 Route 上）
			seedNodes: [],
			// 连线手势内部状态
			connectSource: null,
			connectMade: false,
			connectStartPos: null,
			// 仅初次加载自动适配一次，避免编辑时视图跳动
			hasFitted: false,
			resizeObserver: null,
			resizeTimer: null,
		};
	},
	computed: {
		filteredMaterials() {
			const kw = (this.matKeyword || "").trim().toLowerCase();
			if (!kw) return this.materials;
			return this.materials.filter((m) => (m.full_name || "").toLowerCase().includes(kw));
		},
	},
	watch: {
		routepack() {
			this.reload();
		},
		productId() {
			if (this.routepack) this.reload();
		},
	},
	mounted() {
		this.loadMaterials();
		if (this.routepack) this.reload();
		if (typeof ResizeObserver !== "undefined") {
			this.resizeObserver = new ResizeObserver(() => {
				clearTimeout(this.resizeTimer);
				this.resizeTimer = setTimeout(() => {
					if (this.hasFitted) this.fitToWidth(false);
				}, 80);
			});
			this.resizeObserver.observe(this.$refs.canvas);
		}
	},
	beforeUnmount() {
		if (this.resizeObserver) this.resizeObserver.disconnect();
		clearTimeout(this.resizeTimer);
	},
	methods: {
		/** 加载左侧物料面板列表 */
		loadMaterials() {
			this.matLoading = true;
			this.$API.mtm.material.list
				.req({ page: 0, type__in: this.matType, is_hidden: false, query: "{full_name,id}" })
				.then((res) => {
					this.materials = Array.isArray(res) ? res : res.results || [];
				})
				.finally(() => {
					this.matLoading = false;
				});
		},

		/** 重新拉取路线并重建图 */
		reload() {
			if (!this.routepack) return;
			this.loading = true;
			this.$API.mtm.route.list
				.req({ page: 0, routepack: this.routepack })
				.then((res) => {
					this.routes = Array.isArray(res) ? res : res.results || [];
					this.count = this.routes.length;
					this.buildGraph();
				})
				.finally(() => {
					this.loading = false;
				});
		},

		/** 物料真实类型 → 节点配色：10成品 30原料 其余半成品 */
		kindOfType(t) {
			const s = String(t);
			if (s === "10") return "final";
			if (s === "30") return "raw";
			return "mid";
		},

		/** 拓扑推断兜底（后端未返回物料类型时用）：起点原料 / 终点成品 / 中间半成品 */
		kindOf(id, targetIds) {
			const isSrc = this.routes.some((r) => String(r.material_in) === id);
			const isTgt = targetIds.has(id);
			if (isTgt && !isSrc) return "final";
			if (!isTgt && isSrc) return "raw";
			return "mid";
		},

		/** 由 routes 构建 物料节点 + 工序边，并用 dagre 自动布局；合并未落库的种子节点 */
		buildGraph() {
			const matMap = {};
			const matKind = {};
			const targetIds = new Set();
			const edges = [];
			const parallelEdgeCount = {};

			this.routes.forEach((r) => {
				if (r.material_in == null || r.material_out == null) return;
				const sid = String(r.material_in);
				const tid = String(r.material_out);
				if (!matMap[sid]) matMap[sid] = r.material_in_name || sid;
				if (!matMap[tid]) matMap[tid] = r.material_out_name || tid;
				if (r.material_in_type != null) matKind[sid] = this.kindOfType(r.material_in_type);
				if (r.material_out_type != null) matKind[tid] = this.kindOfType(r.material_out_type);
				targetIds.add(tid);

				const locked = !!r.from_route;
				const edgeKey = sid + "->" + tid;
				const edgeIndex = parallelEdgeCount[edgeKey] || 0;
				parallelEdgeCount[edgeKey] = edgeIndex + 1;
				edges.push({
					id: String(r.id),
					source: sid,
					target: tid,
					label: (r.process_name || "") + (locked ? " 🔒" : ""),
					data: { route: r, locked },
					type: "default",
					pathOptions: { curvature: 0.25 + edgeIndex * 0.12 },
					markerEnd: "arrowclosed",
					style: {
						stroke: locked ? "#c0c4cc" : "#409eff",
						strokeWidth: locked ? 1.7 : 2.2,
						strokeDasharray: locked ? "5 4" : undefined,
					},
					labelStyle: { fill: "#5b6370", fontSize: "12px", fontWeight: 500 },
					labelBgStyle: { fill: "#ffffff", stroke: "#e4e7ed", strokeWidth: 1 },
					labelBgPadding: [7, 4],
					labelBgBorderRadius: 6,
				});
			});

			const routeNodes = Object.keys(matMap).map((id) => ({
				id,
				type: "material",
				data: { label: matMap[id], kind: matKind[id] || this.kindOf(id, targetIds) },
				position: { x: 0, y: 0 },
			}));

			// 工艺包产品常驻画布作为终点节点（尚未被任何工序引用时也显示）
			const pid = this.productId ? String(this.productId) : "";
			if (pid && !matMap[pid]) {
				routeNodes.push({
					id: pid,
					type: "material",
					data: { label: this.productName || "产品", kind: "final" },
					position: { x: 0, y: 0 },
				});
			}

			// 已落库的物料从种子集合中剔除，其余种子保留在画布上
			const routeNodeIds = new Set(Object.keys(matMap));
			if (pid) routeNodeIds.add(pid);
			this.seedNodes = this.seedNodes.filter((s) => !routeNodeIds.has(s.id));

			const prevCount = this.nodes.length;
			this.edges = edges;
			this.nodes = this.layoutNodes(routeNodes.concat(this.seedNodes), edges);
			this.syncStore();
			// 节点增减后 dagre 会整体重排，重新适配视野，避免“看着是空白其实有节点”导致拉线误吸附
			if (this.hasFitted && this.nodes.length !== prevCount) this.fitToWidth();
		},

		/** 编辑与查看使用一致的疏朗布局参数，给工序标签和分支留出空间 */
		layoutNodes(nodes, edges) {
			return layoutGraph(nodes, edges, {
				nodesep: 58,
				edgesep: 32,
				ranksep: 96,
				marginx: 32,
				marginy: 32,
			});
		},

		/** 把本地 nodes/edges 命令式同步到 Vue Flow store（先节点后边，避免边找不到端点） */
		syncStore() {
			this.setEdges([]);
			this.setNodes(this.nodes);
			this.setEdges(this.edges);
		},

		/** 重新布局并适应画布 */
		relayout() {
			this.nodes = this.layoutNodes([...this.nodes], this.edges);
			this.setNodes(this.nodes);
			this.fitToWidth();
		},

		/** 按宽度保持节点可读，长流程通过滚轮或拖动画布纵向浏览 */
		fitToWidth(animated = true) {
			this.$nextTick(() => {
				try {
					const canvas = this.$refs.canvas;
					if (!canvas || !this.nodes.length) return;
					const xs = this.nodes.map((n) => n.position.x);
					const ys = this.nodes.map((n) => n.position.y);
					const minX = Math.min(...xs);
					const maxX = Math.max(...xs) + NODE_W;
					const minY = Math.min(...ys);
					const paddingX = 48;
					const paddingTop = 36;
					const graphWidth = Math.max(maxX - minX, NODE_W);
					const zoom = Math.min(1, Math.max(0.2, (canvas.clientWidth - paddingX * 2) / graphWidth));
					const x = (canvas.clientWidth - graphWidth * zoom) / 2 - minX * zoom;
					const y = paddingTop - minY * zoom;
					this.setViewport(
						{ x, y, zoom },
						{ duration: animated && this.hasFitted ? 200 : 0 }
					);
					this.hasFitted = true;
				} catch (e) {
					/* noop */
				}
			});
		},

		/** VueFlow 节点尺寸测量完成：仅首次自动适配一次 */
		onNodesInit() {
			if (this.hasFitted) return;
			if (this.nodes.length === 0) return;
			this.fitToWidth();
		},

		/* ============ 左侧面板拖拽入画布 ============ */
		onDragStart(evt, mat) {
			evt.dataTransfer.setData(
				"application/xtflow",
				JSON.stringify({ id: mat.id, name: mat.full_name, type: this.matType })
			);
			evt.dataTransfer.effectAllowed = "move";
		},
		onDrop(evt) {
			if (this.readonly) return;
			const raw = evt.dataTransfer.getData("application/xtflow");
			if (!raw) return;
			let mat;
			try {
				mat = JSON.parse(raw);
			} catch (e) {
				return;
			}
			const pos = this.toFlowPos(evt.clientX, evt.clientY);
			this.addSeed(mat, pos);
		},
		toFlowPos(clientX, clientY) {
			if (typeof this.screenToFlowCoordinate === "function") {
				return this.screenToFlowCoordinate({ x: clientX, y: clientY });
			}
			if (typeof this.project === "function") {
				return this.project({ x: clientX, y: clientY });
			}
			return { x: clientX, y: clientY };
		},
		addSeed(mat, position) {
			const id = String(mat.id);
			if (this.nodes.some((n) => n.id === id)) {
				const inUse = this.routes.some(
					(r) => String(r.material_in) === id || String(r.material_out) === id
				);
				this.$message.info(inUse ? "该物料已在工序中使用" : "该物料已在画布中（右键节点可移除）");
				return;
			}
			const node = {
				id,
				type: "material",
				data: { label: mat.name, kind: this.kindOfType(mat.type) },
				position,
			};
			this.seedNodes.push(node);
			this.nodes = [...this.nodes, node];
			// 命令式加入 store：先注册再渲染 Handle，避免 dimensions 竞态
			this.addNodes([node]);
		},

		/* ============ 连线手势 ============ */
		onConnectStart(params) {
			this.connectSource = (params && (params.nodeId || params.source)) || null;
			this.connectMade = false;
			const oe = params && params.event;
			this.connectStartPos = oe && oe.clientX != null ? { x: oe.clientX, y: oe.clientY } : null;
		},
		/** 连到已有节点：新建一条 Route，预填输入/输出物料 */
		onConnect(conn) {
			this.connectMade = true;
			if (this.readonly) return;
			// 误吸附保护：吸回起点自身或目标无效时，按“拉到空白”处理，只带输入
			if (!conn.target || conn.target === conn.source) {
				this.openAddFrom(conn.source);
				return;
			}
			const src = this.nodes.find((n) => n.id === conn.source);
			const tgt = this.nodes.find((n) => n.id === conn.target);
			// 注意：id 为雪花大整数，必须保持字符串，切勿 Number() 否则精度丢失
			this.openForm("add", {
				material_in: conn.source,
				material_in_name: src ? src.data.label : "",
				material_out: conn.target,
				material_out_name: tgt ? tgt.data.label : "",
			});
		},
		/** 拉线到空白处：新建工序，输出物料留空由后端自动生成下一节点 */
		onConnectEnd(evt) {
			const src = this.connectSource;
			const made = this.connectMade;
			const startPos = this.connectStartPos;
			this.connectSource = null;
			this.connectMade = false;
			this.connectStartPos = null;
			if (this.readonly || made || !src) return;
			// 单纯点击手柄(几乎无位移)不算拉线，不弹新增表单
			const oe = evt && evt.clientX != null ? evt : evt && evt.event;
			if (startPos && oe && oe.clientX != null) {
				const dist = Math.hypot(oe.clientX - startPos.x, oe.clientY - startPos.y);
				if (dist < 8) return;
			}
			this.openAddFrom(src);
		},
		/** 以某节点为输入新建工序：只带输入物料，其余字段一律默认值 */
		openAddFrom(srcId) {
			const node = this.nodes.find((n) => n.id === srcId);
			// id 为雪花大整数，保持字符串
			this.openForm("add", {
				material_in: srcId,
				material_in_name: node ? node.data.label : "",
			});
		},

		/** 单击边 => 编辑对应 Route（复用 route_form） */
		onEdgeClick(evt) {
			const edge = evt.edge || evt;
			const route = edge.data && edge.data.route;
			if (!route) return;
			if (this.readonly) {
				this.$message.info("当前状态不可编辑");
				return;
			}
			if (edge.data.locked) {
				this.$message.warning("该工序引用了其他步骤，无法编辑");
				return;
			}
			this.openForm("edit", route);
		},

		/** 右键边 => 删除对应 Route */
		onEdgeContextMenu(evt) {
			const oe = evt.event || evt;
			if (oe && oe.preventDefault) oe.preventDefault();
			if (this.readonly) return;
			const edge = evt.edge || evt;
			const route = edge.data && edge.data.route;
			if (!route) return;
			if (edge.data.locked) {
				this.$message.warning("该工序被其他步骤引用，无法删除");
				return;
			}
			this.$confirm(`确定删除工序「${route.process_name || ""}」吗？`, "提示", {
				type: "warning",
			})
				.then(() => {
					this.$API.mtm.route.delete.req(route.id).then(() => {
						this.$message.success("删除成功");
						this.reload();
						this.$emit("changed");
					});
				})
				.catch(() => {});
		},

		/** 右键节点 => 移除未落库的种子节点（已用于工序的物料需删对应工序） */
		onNodeContextMenu(evt) {
			const oe = evt.event || evt;
			if (oe && oe.preventDefault) oe.preventDefault();
			if (this.readonly) return;
			const node = evt.node || evt;
			const nid = node.id;
			if (this.productId && nid === String(this.productId)) {
				this.$message.warning("产品节点为工艺终点，不可移除");
				return;
			}
			const inUse = this.routes.some(
				(r) => String(r.material_in) === nid || String(r.material_out) === nid
			);
			if (inUse) {
				this.$message.warning("该物料已用于工序，请删除对应工序");
				return;
			}
			const label = (node.data && node.data.label) || "";
			this.$confirm(`从画布移除「${label}」？`, "提示", { type: "warning" })
				.then(() => {
					this.seedNodes = this.seedNodes.filter((s) => s.id !== nid);
					this.nodes = this.nodes.filter((n) => n.id !== nid);
					this.removeNodes([nid]);
				})
				.catch(() => {});
		},

		addStep() {
			this.openForm("add", null);
		},

		/** 打开单步表单；prefill 为 null 表示纯新增。预填经 prop 传入，在表单挂载 open 时一次性应用 */
		openForm(mode, prefill) {
			this.dialogSeq += 1;
			this.formPrefill = prefill || null;
			this.dialogVisible = true;
			this.$nextTick(() => {
				this.$refs.routeForm.open(mode);
			});
		},

		onFormSuccess() {
			this.dialogVisible = false;
			this.reload();
			this.$emit("changed");
		},

		/** 调后端 validate 接口，实时反馈 DAG 是否合法 */
		doValidate() {
			if (!this.routepack) return;
			this.validating = true;
			this.$API.mtm.routepack.validate
				.req(this.routepack)
				.then((res) => {
					if (res && res.valid) {
						this.$message.success("工艺图校验通过");
					} else {
						this.$message.error("校验未通过：" + (res ? res.error : "未知错误"));
					}
				})
				.finally(() => {
					this.validating = false;
				});
		},
	},
};
</script>

<style scoped>
.flow-editor {
	display: flex;
	flex-direction: column;
	width: 100%;
	height: 100%;
	min-height: 480px;
}
.flow-toolbar {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 8px;
	padding: 4px 8px;
	background: #f7f9fc;
	border: 1px solid #eef1f6;
	border-radius: 8px;
	flex-wrap: wrap;
}
.flow-toolbar__left {
	display: flex;
	align-items: center;
	gap: 8px;
	flex-wrap: wrap;
}
.flow-legend {
	display: flex;
	align-items: center;
	gap: 12px;
}
.lg {
	position: relative;
	padding-left: 16px;
	font-size: 12px;
	color: #606266;
}
.lg::before {
	content: "";
	position: absolute;
	left: 0;
	top: 50%;
	transform: translateY(-50%);
	width: 10px;
	height: 10px;
	border-radius: 3px;
	border: 1px solid;
}
.lg--raw::before {
	background: #fdf6ec;
	border-color: #e6a23c;
}
.lg--mid::before {
	background: #ecf5ff;
	border-color: #409eff;
}
.lg--final::before {
	background: #f0f9eb;
	border-color: #67c23a;
}
.flow-tip {
	color: #909399;
	font-size: 12px;
	padding: 3px 2px 4px;
}
.flow-tip--ro {
	color: #e6a23c;
}
.flow-body {
	display: flex;
	flex: 1;
	min-height: 420px;
	gap: 6px;
}
.flow-palette {
	width: 190px;
	flex: 0 0 190px;
	display: flex;
	flex-direction: column;
	border: 1px solid #eef1f6;
	border-radius: 8px;
	overflow: hidden;
	background: #fff;
}
.palette-head {
	padding: 8px;
	border-bottom: 1px solid #f0f2f5;
	background: #fafbfd;
}
.palette-title {
	font-size: 13px;
	font-weight: 600;
	color: #303133;
	margin-bottom: 8px;
}
.palette-list {
	flex: 1;
	overflow-y: auto;
	padding: 8px;
}
.palette-item {
	display: flex;
	align-items: center;
	gap: 6px;
	padding: 7px 9px;
	margin-bottom: 6px;
	border: 1px solid #ebeef5;
	border-radius: 6px;
	background: #fff;
	cursor: grab;
	font-size: 12px;
	color: #303133;
	transition: all 0.15s;
}
.palette-item:hover {
	border-color: #409eff;
	background: #ecf5ff;
	box-shadow: 0 2px 6px rgba(64, 158, 255, 0.15);
	transform: translateY(-1px);
}
.palette-item:active {
	cursor: grabbing;
}
.palette-item__grip {
	color: #c0c4cc;
	flex-shrink: 0;
}
.palette-item__txt {
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}
.palette-empty {
	color: #909399;
	font-size: 12px;
	text-align: center;
	padding: 16px 0;
}
.flow-canvas {
	position: relative;
	flex: 1;
	min-height: 420px;
	border: 1px solid #eef1f6;
	border-radius: 8px;
	overflow: hidden;
	background: #fbfcfe;
}
.flow-empty {
	position: absolute;
	top: 50%;
	left: 50%;
	transform: translate(-50%, -50%);
	color: #a8abb2;
	font-size: 13px;
	text-align: center;
	pointer-events: none;
}
.flow-empty__icon {
	font-size: 34px;
	color: #c8ccd4;
	display: block;
	margin-bottom: 10px;
}

</style>
<style>
/* Vue Flow 内部元素（非 scoped）；物料节点样式在 flow/MaterialNode.vue */
.vue-flow__controls {
	box-shadow: 0 2px 10px rgba(31, 45, 61, 0.12);
	border-radius: 6px;
	overflow: hidden;
}
</style>
