import { nextTick } from "vue";
import { v4 as uuid } from "uuid";
import { MarkerType } from "@vue-flow/core";

const clone = (value) => JSON.parse(JSON.stringify(value ?? {}));

const GROUP_TYPES = new Set(["loop", "selectArea"]);
const TARGET_ONLY_TYPES = new Set(["end", "stopLoop", "subEnd"]);
const SOURCE_ONLY_TYPES = new Set(["start", "subStart"]);
const LOOP_BODY_INSET = { left: 24, top: 112, right: 24, bottom: 24 };
const GROUP_BODY_INSET = { left: 24, top: 120, right: 24, bottom: 24 };
const LOOP_MIN_SIZE = { width: 520, height: 360 };

const loopChildExtent = () => ({
  range: "parent",
  padding: [
    LOOP_BODY_INSET.top,
    LOOP_BODY_INSET.right,
    LOOP_BODY_INSET.bottom,
    LOOP_BODY_INSET.left,
  ],
});

const groupChildExtent = () => ({
  range: "parent",
  padding: [
    GROUP_BODY_INSET.top,
    GROUP_BODY_INSET.right,
    GROUP_BODY_INSET.bottom,
    GROUP_BODY_INSET.left,
  ],
});

const nodeSize = (node) => {
  const width = Number(node.properties?.width || node.width);
  const height = Number(node.properties?.height || node.height);
  if (Number.isFinite(width) && Number.isFinite(height)) {
    return { width, height };
  }
  if (node.type === "branch") return { width: 748, height: 420 };
  if (GROUP_TYPES.has(node.type)) return { width: 720, height: 460 };
  if (TARGET_ONLY_TYPES.has(node.type) || SOURCE_ONLY_TYPES.has(node.type)) {
    return { width: 328, height: 120 };
  }
  return { width: 372, height: 180 };
};

const edgeId = () => `edge_${uuid().replace(/-/g, "")}`;
const nodeId = () => uuid().replace(/-/g, "");

const toVueNode = (node, sizeOverride, positionOverride) => {
  const normalized = clone(node);
  normalized.id ||= nodeId();
  normalized.properties ||= {};
  const overrideWidth = Number(sizeOverride?.width);
  const overrideHeight = Number(sizeOverride?.height);
  const size = Number.isFinite(overrideWidth) && overrideWidth > 0
    && Number.isFinite(overrideHeight) && overrideHeight > 0
    ? { width: overrideWidth, height: overrideHeight }
    : nodeSize(normalized);
  const center = {
    x: Number(normalized.x) || 0,
    y: Number(normalized.y) || 0,
  };
  const overrideX = Number(positionOverride?.x);
  const overrideY = Number(positionOverride?.y);
  const position = Number.isFinite(overrideX) && Number.isFinite(overrideY)
    ? { x: overrideX, y: overrideY }
    : {
      x: center.x - size.width / 2,
      y: center.y - size.height / 2,
    };

  const dimensions = GROUP_TYPES.has(normalized.type)
    ? { width: size.width, height: size.height }
    : { width: size.width };

  return {
    id: normalized.id,
    type: "flow-node",
    position,
    ...dimensions,
    draggable: normalized.draggable !== false,
    selectable: true,
    deletable: !SOURCE_ONLY_TYPES.has(normalized.type) && normalized.type !== "end",
    connectable: true,
    zIndex: GROUP_TYPES.has(normalized.type) ? 0 : 2,
    data: {
      flowType: normalized.type,
      properties: normalized.properties,
      children: Array.isArray(normalized.children) ? normalized.children : [],
      text: normalized.text,
      backend: normalized,
      originalCenter: center,
      originalPosition: position,
      size,
    },
  };
};

const toVueEdge = (edge) => {
  const normalized = clone(edge);
  normalized.id ||= edgeId();
  return {
    id: normalized.id,
    source: normalized.sourceNodeId,
    target: normalized.targetNodeId,
    sourceHandle: normalized.sourceAnchorId || null,
    targetHandle: normalized.targetAnchorId || null,
    type: "flow-edge",
    animated: true,
    selectable: true,
    focusable: true,
    deletable: true,
    interactionWidth: 32,
    markerEnd: {
      type: MarkerType.ArrowClosed,
      color: "#ff6b35",
      width: 20,
      height: 20,
    },
    style: { stroke: "#ff6b35", strokeWidth: 2 },
    data: { backend: normalized },
  };
};

const attachChildrenToGroups = (nodes) => {
  const nodeLookup = new Map(nodes.map((node) => [node.id, node]));
  const declaredParents = new Map();

  nodes.filter((node) => GROUP_TYPES.has(node.data.flowType)).forEach((group) => {
    (group.data.children || []).forEach((childId) => {
      if (!declaredParents.has(childId)) declaredParents.set(childId, group.id);
    });
  });

  nodes.forEach((child) => {
    const parentId = child.data.properties?.parentId || declaredParents.get(child.id);
    const parent = nodeLookup.get(parentId);
    if (!parent || !GROUP_TYPES.has(parent.data.flowType) || GROUP_TYPES.has(child.data.flowType)) return;

    const isLoop = parent.data.flowType === "loop";
    const relativePosition = {
      x: isLoop
        ? Math.max(child.position.x - parent.position.x, LOOP_BODY_INSET.left)
        : child.position.x - parent.position.x,
      y: isLoop
        ? Math.max(child.position.y - parent.position.y, LOOP_BODY_INSET.top)
        : child.position.y - parent.position.y,
    };
    child.parentNode = parentId;
    child.extent = isLoop ? loopChildExtent() : groupChildExtent();
    child.expandParent = false;
    child.position = relativePosition;
    child.zIndex = 5;
    child.data.properties = { ...child.data.properties, parentId };
    parent.data.children = Array.from(new Set([...(parent.data.children || []), child.id]));
  });

  nodes.filter((node) => node.data.flowType === "loop").forEach((group) => {
    const children = nodes.filter((node) => node.parentNode === group.id);
    if (!children.length) return;
    const requiredWidth = Math.max(...children.map((child) => (
      child.position.x + Number(child.data.size?.width || 0) + LOOP_BODY_INSET.right
    )));
    const requiredHeight = Math.max(...children.map((child) => (
      child.position.y + Number(child.data.size?.height || 0) + LOOP_BODY_INSET.bottom
    )));
    const width = Math.max(Number(group.data.size?.width || 0), LOOP_MIN_SIZE.width, requiredWidth);
    const height = Math.max(Number(group.data.size?.height || 0), LOOP_MIN_SIZE.height, requiredHeight);
    group.width = width;
    group.height = height;
    group.data.size = { width, height };
    group.data.properties = { ...group.data.properties, width, height };
  });

  return nodes.sort((left, right) => Number(Boolean(left.parentNode)) - Number(Boolean(right.parentNode)));
};

export const toVueFlowData = (graph = {}, options = {}) => {
  const nodes = attachChildrenToGroups((graph.nodes || []).map((node) => (
    toVueNode(node, options.nodeSizes?.get(node.id), options.nodePositions?.get(node.id))
  )));
  const nodeLookup = new Map(nodes.map((node) => [node.id, node]));
  const edges = (graph.edges || []).map((edge) => {
    const vueEdge = toVueEdge(edge);
    const source = nodeLookup.get(vueEdge.source);
    const target = nodeLookup.get(vueEdge.target);
    if (source?.parentNode || target?.parentNode) vueEdge.zIndex = 4;
    return vueEdge;
  });
  return { nodes, edges };
};

export const createFlowFacade = (store) => {
  const handlers = new Map();
  const componentInstances = new Map();
  const loopLayoutStates = new Map();
  let historySuspended = false;
  let history = [];
  let historyIndex = -1;

  const getNodes = () => store.getNodes.value || [];
  const getEdges = () => store.getEdges.value || [];

  const backendNode = (node) => {
    const result = clone(node.data.backend);
    const size = {
      width: Number(node.width || node.dimensions?.width || node.data.size?.width || 0),
      height: Number(node.height || node.dimensions?.height || node.data.size?.height || 0),
    };
    const parent = node.parentNode ? store.findNode(node.parentNode) : null;
    const parentPosition = parent?.position || parent?.computedPosition || { x: 0, y: 0 };
    const absolutePosition = parent
      ? { x: parentPosition.x + node.position.x, y: parentPosition.y + node.position.y }
      : (node.position || node.computedPosition);
    result.id = node.id;
    result.type = node.data.flowType;
    if (GROUP_TYPES.has(result.type)) {
      result.x = absolutePosition.x + size.width / 2;
      result.y = absolutePosition.y + size.height / 2;
    } else {
      const originalCenter = node.data.originalCenter || {
        x: absolutePosition.x + size.width / 2,
        y: absolutePosition.y + size.height / 2,
      };
      const originalPosition = node.data.originalPosition || absolutePosition;
      result.x = originalCenter.x + absolutePosition.x - originalPosition.x;
      result.y = originalCenter.y + absolutePosition.y - originalPosition.y;
    }
    result.properties = clone(node.data.properties || {});
    if (parent?.data?.flowType === "selectArea") delete result.properties.parentId;
    if (GROUP_TYPES.has(result.type)) {
      result.properties.width = size.width;
      result.properties.height = size.height;
    }
    if (node.data.children?.length || GROUP_TYPES.has(result.type)) {
      result.children = [...(node.data.children || [])];
    } else {
      delete result.children;
    }
    return result;
  };

  const backendEdge = (edge) => ({
    ...clone(edge.data?.backend),
    id: edge.id,
    type: edge.data?.backend?.type || "bezier",
    sourceNodeId: edge.source,
    targetNodeId: edge.target,
    sourceAnchorId: edge.sourceHandle || `${edge.source}_1`,
    targetAnchorId: edge.targetHandle || `${edge.target}_3`,
  });

  const emit = (name, payload) => {
    (handlers.get(name) || []).forEach((handler) => handler(payload));
  };

  const recordHistory = () => {
    if (historySuspended) return;
    const graph = facade.getGraphData();
    const serialized = JSON.stringify(graph);
    if (history[historyIndex]?.serialized === serialized) return;
    history = history.slice(0, historyIndex + 1);
    history.push({ serialized, graph: clone(graph) });
    if (history.length > 100) history.shift();
    historyIndex = history.length - 1;
  };

  const changed = () => {
    if (historySuspended) return;
    recordHistory();
    emit("history:change", { data: facade.getGraphData() });
  };

  const getNodeRect = (node) => {
    const parent = node.parentNode ? store.findNode(node.parentNode) : null;
    const parentPosition = parent?.position || parent?.computedPosition || { x: 0, y: 0 };
    const position = parent
      ? { x: parentPosition.x + node.position.x, y: parentPosition.y + node.position.y }
      : (node.position || node.computedPosition);
    return {
      x: Number(position?.x || 0),
      y: Number(position?.y || 0),
      width: Number(node.width || node.dimensions?.width || node.data.size?.width || 0),
      height: Number(node.height || node.dimensions?.height || node.data.size?.height || 0),
    };
  };

  const getLoopLayoutState = (group) => {
    const current = getNodeRect(group);
    const existing = loopLayoutStates.get(group.id);
    if (!existing) {
      const state = { base: { ...current }, last: { ...current } };
      loopLayoutStates.set(group.id, state);
      return state;
    }

    const sizeChanged = Math.abs(current.width - existing.last.width) > 1
      || Math.abs(current.height - existing.last.height) > 1;
    if (sizeChanged) {
      existing.base = { ...current };
    } else {
      const dx = current.x - existing.last.x;
      const dy = current.y - existing.last.y;
      existing.base.x += dx;
      existing.base.y += dy;
    }
    existing.last = { ...current };
    return existing;
  };

  const layoutLoop = (group, activeChildId, activePosition) => {
    const state = getLoopLayoutState(group);
    const children = getNodes().filter((node) => node.parentNode === group.id);
    if (!children.length) return false;

    const snapshots = children.map((child) => {
      const rect = getNodeRect(child);
      const position = child.id === activeChildId && activePosition
        ? { x: activePosition.x, y: activePosition.y }
        : { x: rect.x, y: rect.y };
      return { child, position, width: rect.width, height: rect.height };
    });
    const contentLeft = Math.min(...snapshots.map(({ position }) => position.x));
    const contentTop = Math.min(...snapshots.map(({ position }) => position.y));
    const contentRight = Math.max(...snapshots.map(({ position, width }) => position.x + width));
    const contentBottom = Math.max(...snapshots.map(({ position, height }) => position.y + height));
    const left = Math.min(state.base.x, contentLeft - LOOP_BODY_INSET.left);
    const top = Math.min(state.base.y, contentTop - LOOP_BODY_INSET.top);
    const right = Math.max(
      state.base.x + Math.max(state.base.width, LOOP_MIN_SIZE.width),
      contentRight + LOOP_BODY_INSET.right,
    );
    const bottom = Math.max(
      state.base.y + Math.max(state.base.height, LOOP_MIN_SIZE.height),
      contentBottom + LOOP_BODY_INSET.bottom,
    );
    const width = right - left;
    const height = bottom - top;
    const properties = { ...group.data.properties, width, height };

    store.updateNode(group.id, {
      position: { x: left, y: top },
      computedPosition: { ...group.computedPosition, x: left, y: top },
      dimensions: { width, height },
      width,
      height,
    });
    store.updateNodeData(group.id, {
      ...group.data,
      properties,
      size: { width, height },
    }, { replace: true });
    snapshots.forEach(({ child, position }) => {
      store.updateNode(child.id, {
        position: { x: position.x - left, y: position.y - top },
        computedPosition: { ...child.computedPosition, x: position.x, y: position.y },
        extent: loopChildExtent(),
        expandParent: false,
      });
    });
    state.last = { x: left, y: top, width, height };
    nextTick(() => store.updateNodeInternals([group.id, ...children.map((child) => child.id)]));
    return true;
  };

  const getAnchors = (node) => {
    const type = node.data.flowType;
    if (type === "branch") {
      return [
        { id: `${node.id}_entry`, name: "left" },
        ...(node.data.properties?.anchor || []).map((item) => ({ ...item, name: "right" })),
      ];
    }
    if (TARGET_ONLY_TYPES.has(type)) return [{ id: `${node.id}_3`, name: "left" }];
    if (SOURCE_ONLY_TYPES.has(type)) return [{ id: `${node.id}_1`, name: "right" }];
    if (type === "selectArea") return [];
    return [
      { id: `${node.id}_1`, name: "right" },
      { id: `${node.id}_3`, name: "left" },
    ];
  };

  const modelFor = (id) => {
    const node = store.findNode(id);
    if (!node) return null;
    const model = {
      id,
      type: node.data.flowType,
      get x() { return backendNode(node).x; },
      get y() { return backendNode(node).y; },
      get width() { return node.dimensions?.width || node.data.size?.width || 0; },
      get height() { return node.dimensions?.height || node.data.size?.height || 0; },
      get left() { return getNodeRect(node).x; },
      get top() { return getNodeRect(node).y; },
      get parentId() { return node.parentNode || null; },
      get anchors() { return getAnchors(node); },
      get children() { return new Set(node.data.children || []); },
      set children(value) {
        const children = Array.from(value || []);
        store.updateNodeData(id, { ...node.data, children }, { replace: true });
        changed();
      },
      get draggable() { return node.draggable !== false; },
      set draggable(value) { store.updateNode(id, { draggable: Boolean(value) }); },
      getProperties: () => facade.getProperties(id),
      setProperties: (properties) => facade.setProperties(id, properties),
      setComponentInstance: (instance) => componentInstances.set(id, instance),
      closePopover: () => componentInstances.get(id)?.closePopover?.(),
      updateSize: () => store.updateNodeInternals([id]),
      setCustomProperties: () => store.updateNodeInternals([id]),
    };
    return model;
  };

  const facade = {
    on(name, handler) {
      const list = handlers.get(name) || [];
      list.push(handler);
      handlers.set(name, list);
      return () => facade.off(name, handler);
    },
    once(name, handler) {
      const wrapped = (payload) => {
        facade.off(name, wrapped);
        handler(payload);
      };
      facade.on(name, wrapped);
    },
    off(name, handler) {
      if (!handler) handlers.delete(name);
      else handlers.set(name, (handlers.get(name) || []).filter((item) => item !== handler));
    },
    emit,
    markChanged: changed,
    getGraphData() {
      return {
        nodes: getNodes().map(backendNode),
        edges: getEdges().map(backendEdge),
      };
    },
    render(graph, options = {}) {
      historySuspended = true;
      loopLayoutStates.clear();
      const nodeSizes = options.preserveNodeSizes === true
        ? new Map(getNodes().map((node) => [node.id, {
          width: Number(node.dimensions?.width || node.width || node.data.size?.width || 0),
          height: Number(node.dimensions?.height || node.height || node.data.size?.height || 0),
        }]))
        : options.nodeSizes;
      const nodePositions = options.preserveNodePositions === true
        ? new Map(getNodes().map((node) => {
          const rect = getNodeRect(node);
          return [node.id, { x: rect.x, y: rect.y }];
        }))
        : options.nodePositions;
      const data = toVueFlowData(graph, { nodeSizes, nodePositions });
      const finishRender = () => {
        historySuspended = false;
        if (options.resetHistory !== false) {
          history = [];
          historyIndex = -1;
          recordHistory();
        }
        nextTick(() => store.updateNodeInternals());
      };
      const applyData = () => {
        store.setNodes(data.nodes);
        store.setEdges(data.edges);
        finishRender();
      };

      if (options.replaceNodes === true) {
        // Changing parentNode is structural in Vue Flow. Unmount in one tick
        // and remount in the next so node wrappers and edges cannot retain
        // different graph-node instances for the same id.
        store.setEdges([]);
        store.setNodes([]);
        nextTick(applyData);
      } else {
        applyData();
      }
    },
    addNode(node) {
      const vueNode = toVueNode(node);
      store.addNodes(vueNode);
      changed();
      return backendNode(vueNode);
    },
    deleteNode(id) {
      const target = store.findNode(id);
      if (target?.data?.flowType === "selectArea") {
        facade.ungroup(id);
        return;
      }
      const parentId = target?.parentNode;
      const childIds = target?.data?.children || [];
      const removeChildren = target?.data?.flowType === "loop" && childIds.length > 0;
      const nodeIds = removeChildren ? [id, ...childIds] : id;
      const removedIds = new Set(Array.isArray(nodeIds) ? nodeIds : [nodeIds]);
      store.removeNodes(nodeIds, true, removeChildren);
      getNodes().forEach((node) => {
        if (node.data.children?.includes(id) && !removedIds.has(node.id)) {
          store.updateNodeData(node.id, {
            ...node.data,
            children: node.data.children.filter((childId) => childId !== id),
          }, { replace: true });
        }
      });
      componentInstances.delete(id);
      childIds.forEach((childId) => componentInstances.delete(childId));
      changed();
      if (parentId) nextTick(() => facade.resizeGroupToChildren(parentId));
    },
    addEdge(edge) {
      const vueEdge = toVueEdge(edge);
      const source = store.findNode(vueEdge.source);
      const target = store.findNode(vueEdge.target);
      if (source?.parentNode || target?.parentNode) vueEdge.zIndex = 4;
      store.addEdges(vueEdge);
      changed();
      return backendEdge(vueEdge);
    },
    deleteEdge(id) {
      store.removeEdges(id);
      changed();
    },
    deleteEdgeByNodeId(id) {
      store.removeEdges(getEdges().filter((edge) => edge.source === id || edge.target === id));
      changed();
    },
    getProperties(id) {
      return clone(store.findNode(id)?.data?.properties || {});
    },
    setProperties(id, properties) {
      const node = store.findNode(id);
      if (!node) return;
      store.updateNodeData(id, { ...node.data, properties: clone(properties) }, { replace: true });
      nextTick(() => store.updateNodeInternals([id]));
      changed();
    },
    getNodeModelById: modelFor,
    getSelectElements() {
      return {
        nodes: (store.getSelectedNodes.value || []).map(backendNode),
        edges: (store.getSelectedEdges.value || []).map(backendEdge),
      };
    },
    clearSelectElements() {
      store.removeSelectedElements();
    },
    selectElementById(id) {
      store.removeSelectedElements();
      if (!id) return;
      const node = store.findNode(id);
      if (node) store.addSelectedNodes([node]);
      const edge = store.findEdge(id);
      if (edge) store.addSelectedEdges([edge]);
    },
    focusOn({ id }) {
      const node = store.findNode(id);
      if (!node) return;
      const center = backendNode(node);
      store.setCenter(center.x, center.y, { zoom: Math.max(store.viewport.value?.zoom || 1, 0.7), duration: 300 });
    },
    fitView() {
      return store.fitView({ padding: 0.18, duration: 300 });
    },
    translateCenter() {
      return facade.fitView();
    },
    zoom(scale, center) {
      if (Array.isArray(center) && center.length === 2) {
        return store.setCenter(center[0], center[1], { zoom: scale, duration: 250 });
      }
      return store.zoomTo(scale, { duration: 250 });
    },
    getPointByClient(x, y) {
      const point = store.screenToFlowCoordinate({ x, y });
      return { canvasOverlayPosition: point, domOverlayPosition: point };
    },
    addToGroup(groupId, childId, dropCenter) {
      const group = store.findNode(groupId);
      const child = store.findNode(childId);
      if (!group || !child || GROUP_TYPES.has(child.data.flowType)) return;
      const graph = facade.getGraphData();
      const groupData = graph.nodes.find((node) => node.id === groupId);
      const childData = graph.nodes.find((node) => node.id === childId);
      if (!groupData || !childData) return;
      groupData.children = Array.from(new Set([...(groupData.children || []), childId]));
      childData.properties = { ...(childData.properties || {}), parentId: groupId };
      if (dropCenter) {
        childData.x = dropCenter.x;
        childData.y = dropCenter.y;
      }
      facade.render(graph, {
        resetHistory: false,
        replaceNodes: true,
        preserveNodeSizes: true,
        preserveNodePositions: true,
      });
      nextTick(() => {
        store.updateNodeInternals([groupId, childId]);
        changed();
      });
    },
    groupNodes(groupNode, childIds = []) {
      const graph = facade.getGraphData();
      const selectedIds = new Set(childIds);
      const children = graph.nodes.filter((node) => (
        selectedIds.has(node.id)
        && !GROUP_TYPES.has(node.type)
        && !store.findNode(node.id)?.parentNode
      ));
      if (!children.length) return null;

      const group = clone(groupNode);
      group.id ||= nodeId();
      group.type = "selectArea";
      group.properties = { ...(group.properties || {}), action: "NONE" };
      group.children = children.map((node) => node.id);
      children.forEach((node) => {
        node.properties = { ...(node.properties || {}), parentId: group.id };
      });
      graph.nodes.push(group);
      facade.render(graph, {
        resetHistory: false,
        replaceNodes: true,
        preserveNodeSizes: true,
        preserveNodePositions: true,
      });
      nextTick(() => {
        store.updateNodeInternals([group.id, ...group.children]);
        changed();
      });
      return clone(group);
    },
    ungroup(groupId) {
      const graph = facade.getGraphData();
      const group = graph.nodes.find((node) => node.id === groupId && node.type === "selectArea");
      if (!group) return false;
      const childIds = new Set([
        ...(group.children || []),
        ...graph.nodes.filter((node) => node.properties?.parentId === groupId).map((node) => node.id),
      ]);
      graph.nodes = graph.nodes.filter((node) => node.id !== groupId);
      graph.nodes.forEach((node) => {
        if (!childIds.has(node.id)) return;
        const properties = { ...(node.properties || {}) };
        delete properties.parentId;
        node.properties = properties;
      });
      facade.render(graph, {
        resetHistory: false,
        replaceNodes: true,
        preserveNodeSizes: true,
        preserveNodePositions: true,
      });
      nextTick(() => {
        store.updateNodeInternals([...childIds]);
        changed();
      });
      return true;
    },
    prepareLoopChildDrag(childId) {
      const child = store.findNode(childId);
      const group = child?.parentNode ? store.findNode(child.parentNode) : null;
      if (!group || group.data.flowType !== "loop") return;
      store.updateNode(childId, {
        extent: loopChildExtent(),
        expandParent: false,
      });
      getLoopLayoutState(group);
      const rect = getNodeRect(child);
      return { groupId: group.id, position: { x: rect.x, y: rect.y } };
    },
    layoutLoopDuringDrag(groupId, childId, position) {
      const group = store.findNode(groupId);
      if (!group || group.data.flowType !== "loop" || !position) return;
      layoutLoop(group, childId, position);
    },
    resizeGroupToChildren(groupId, options = {}) {
      const group = store.findNode(groupId);
      if (!group || group.data.flowType !== "loop") return;
      if (layoutLoop(group) && options.markChanged !== false) changed();
    },
    moveNodeBy(id, dx, dy) {
      const node = store.findNode(id);
      if (!node) return;
      store.updateNode(id, {
        position: {
          x: node.position.x + dx,
          y: node.position.y + dy,
        },
      });
      changed();
    },
    undo() {
      if (historyIndex <= 0) return false;
      historyIndex -= 1;
      const snapshot = clone(history[historyIndex].graph);
      facade.render(snapshot, { resetHistory: false, replaceNodes: true });
      nextTick(() => emit("history:change", { data: facade.getGraphData() }));
      return true;
    },
    redo() {
      if (historyIndex >= history.length - 1) return false;
      historyIndex += 1;
      const snapshot = clone(history[historyIndex].graph);
      facade.render(snapshot, { resetHistory: false, replaceNodes: true });
      nextTick(() => emit("history:change", { data: facade.getGraphData() }));
      return true;
    },
    registerComponentInstance(id, instance) {
      if (instance) componentInstances.set(id, instance);
    },
    extension: {
      selectionSelect: {
        openSelectionSelect: () => emit("selection:open"),
        closeSelectionSelect: () => emit("selection:close"),
      },
    },
  };

  return facade;
};
