/**
 * Vue node lifecycle management for LiteGraph integration
 * Provides event-driven reactivity with performance optimizations
 */
import { reactiveComputed } from '@vueuse/core'
import { reactive, shallowReactive } from 'vue'

import { useChainCallback } from '@/composables/functional/useChainCallback'
import type { PromotedWidgetSource } from '@/core/graph/subgraph/promotedWidgetTypes'
import { isPromotedWidgetView } from '@/core/graph/subgraph/promotedWidgetTypes'
import { matchPromotedInput } from '@/core/graph/subgraph/matchPromotedInput'
import {
  resolveConcretePromotedWidget,
  resolvePromotedWidgetSource
} from '@/core/graph/subgraph/resolveConcretePromotedWidget'
import { resolveSubgraphInputTarget } from '@/core/graph/subgraph/resolveSubgraphInputTarget'
import type {
  INodeInputSlot,
  INodeOutputSlot
} from '@/lib/litegraph/src/interfaces'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import { LayoutSource } from '@/renderer/core/layout/types'
import type { NodeId } from '@/renderer/core/layout/types'
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { isDOMWidget } from '@/scripts/domWidget'
import { IS_CONTROL_WIDGET } from '@/scripts/widgets'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import type { WidgetValue, SafeControlWidget } from '@/types/simplifiedWidget'
import { normalizeControlOption } from '@/types/simplifiedWidget'
import { getWidgetEntityIdForNode } from '@/utils/litegraphUtil'
import type { WidgetEntityId } from '@/world/entityIds'

import type {
  LGraph,
  LGraphBadge,
  LGraphNode,
  LGraphTriggerAction,
  LGraphTriggerEvent,
  LGraphTriggerParam
} from '@/lib/litegraph/src/litegraph'
import type { TitleMode } from '@/lib/litegraph/src/types/globalEnums'
import { NodeSlotType } from '@/lib/litegraph/src/types/globalEnums'
import { app } from '@/scripts/app'
import { getExecutionIdByNode } from '@/utils/graphTraversalUtil'

export interface WidgetSlotMetadata {
  index: number
  linked: boolean
  originNodeId?: string
  originOutputName?: string
  type: string
}

type Badges = (LGraphBadge | (() => LGraphBadge))[]

/**
 * Minimal render-specific widget data extracted from LiteGraph widgets.
 * Value and metadata (label, hidden, disabled, etc.) are accessed via widgetValueStore.
 */
export interface SafeWidgetData {
  entityId?: WidgetEntityId
  nodeId?: NodeId
  name: string
  type: string
  /** Callback to invoke when widget value changes (wraps LiteGraph callback + triggerDraw) */
  callback?: ((value: unknown) => void) | undefined
  /** Control widget for seed randomization/increment/decrement */
  controlWidget?: SafeControlWidget
  /** Whether widget has custom layout size computation */
  hasLayoutSize?: boolean
  /** Whether widget is a DOM widget */
  isDOMWidget?: boolean
  /**
   * Widget options needed for render decisions.
   * Note: Most metadata should be accessed via widgetValueStore.getWidget().
   */
  options?: {
    canvasOnly?: boolean
    advanced?: boolean
    hidden?: boolean
    read_only?: boolean
  }
  /** Input specification from node definition */
  spec?: InputSpec
  /** Input slot metadata (index and link status) */
  slotMetadata?: WidgetSlotMetadata
  /**
   * Original LiteGraph widget name used for slot metadata matching.
   * For promoted widgets, `name` is `sourceWidgetName` (interior widget name)
   * which differs from the subgraph node's input slot widget name.
   */
  slotName?: string
  /**
   * Execution ID of the interior node that owns the source widget.
   * Only set for promoted widgets where the source node differs from the
   * host subgraph node. Used for missing-model lookups that key by
   * execution ID (e.g. `"65:42"` vs the host node's `"65"`).
   */
  sourceExecutionId?: string
  /** Tooltip text from the resolved widget. */
  tooltip?: string
  /** For promoted widgets, the display label from the subgraph input slot. */
  promotedLabel?: string
}

export interface VueNodeData {
  executing: boolean
  id: NodeId
  mode: number
  selected: boolean
  title: string
  type: string
  apiNode?: boolean
  badges?: Badges
  bgcolor?: string
  color?: string
  flags?: {
    collapsed?: boolean
    ghost?: boolean
    pinned?: boolean
  }
  hasErrors?: boolean
  inputs?: INodeInputSlot[]
  outputs?: INodeOutputSlot[]
  resizable?: boolean
  shape?: number
  showAdvanced?: boolean
  subgraphId?: string | null
  titleMode?: TitleMode
  widgets?: SafeWidgetData[]
}

export interface GraphNodeManager {
  // Reactive state - safe data extracted from LiteGraph nodes
  vueNodeData: ReadonlyMap<string, VueNodeData>

  // Access to original LiteGraph nodes (non-reactive)
  getNode(id: string): LGraphNode | undefined

  // Lifecycle methods
  cleanup(): void
}

function isPromotedDOMWidget(widget: IBaseWidget): boolean {
  if (!isPromotedWidgetView(widget)) return false
  const sourceWidget = resolvePromotedWidgetSource(widget.node, widget)
  if (!sourceWidget) return false

  const innerWidget = sourceWidget.widget
  return (
    ('element' in innerWidget && !!innerWidget.element) ||
    ('component' in innerWidget && !!innerWidget.component)
  )
}

export function getControlWidget(
  widget: IBaseWidget
): SafeControlWidget | undefined {
  const cagWidget = widget.linkedWidgets?.find((w) => w[IS_CONTROL_WIDGET])
  if (!cagWidget) return
  return {
    value: normalizeControlOption(cagWidget.value),
    update: (value) => (cagWidget.value = normalizeControlOption(value))
  }
}

interface SharedWidgetEnhancements {
  controlWidget?: SafeControlWidget
  spec?: InputSpec
}

function getSharedWidgetEnhancements(
  node: LGraphNode,
  widget: IBaseWidget
): SharedWidgetEnhancements {
  const nodeDefStore = useNodeDefStore()

  return {
    controlWidget: getControlWidget(widget),
    spec: nodeDefStore.getInputSpecForWidget(node, widget.name)
  }
}

/**
 * Validates that a value is a valid WidgetValue type
 */
function normalizeWidgetValue(value: unknown): WidgetValue {
  if (value === null || value === undefined || value === void 0) {
    return undefined
  }
  if (
    typeof value === 'string' ||
    typeof value === 'number' ||
    typeof value === 'boolean'
  ) {
    return value
  }
  if (typeof value === 'object') {
    // Check if it's a File array
    if (
      Array.isArray(value) &&
      value.length > 0 &&
      value.every((item): item is File => item instanceof File)
    ) {
      return value
    }
    // Otherwise it's a generic object
    return value
  }
  // If none of the above, return undefined
  console.warn(`Invalid widget value type: ${typeof value}`, value)
  return undefined
}

function safeWidgetMapper(
  node: LGraphNode,
  slotMetadata: Map<string, WidgetSlotMetadata>
): (widget: IBaseWidget) => SafeWidgetData {
  function extractWidgetDisplayOptions(
    widget: IBaseWidget
  ): SafeWidgetData['options'] {
    if (!widget.options) return undefined

    return {
      canvasOnly: widget.options.canvasOnly,
      advanced: widget.options?.advanced ?? widget.advanced,
      hidden: widget.options.hidden,
      read_only: widget.options.read_only
    }
  }

  function resolvePromotedSourceByInputName(
    inputName: string
  ): PromotedWidgetSource | null {
    const resolvedTarget = resolveSubgraphInputTarget(node, inputName)
    if (!resolvedTarget) return null

    return {
      sourceNodeId: resolvedTarget.nodeId,
      sourceWidgetName: resolvedTarget.widgetName
    }
  }

  function resolvePromotedWidgetIdentity(widget: IBaseWidget): {
    displayName: string
    promotedSource: PromotedWidgetSource | null
  } {
    if (!isPromotedWidgetView(widget)) {
      return {
        displayName: widget.name,
        promotedSource: null
      }
    }

    const matchedInput = matchPromotedInput(node.inputs, widget)
    const promotedInputName = matchedInput?.name
    const displayName = promotedInputName ?? widget.name
    const directSource: PromotedWidgetSource = {
      sourceNodeId: widget.sourceNodeId,
      sourceWidgetName: widget.sourceWidgetName
    }
    const promotedSource =
      matchedInput?._widget === widget
        ? (resolvePromotedSourceByInputName(displayName) ?? directSource)
        : directSource

    return {
      displayName,
      promotedSource
    }
  }

  return function (widget) {
    try {
      const { displayName, promotedSource } =
        resolvePromotedWidgetIdentity(widget)

      // Get shared enhancements (controlWidget, spec, nodeType)
      const sharedEnhancements = getSharedWidgetEnhancements(node, widget)
      const slotInfo =
        slotMetadata.get(displayName) ?? slotMetadata.get(widget.name)

      // Wrapper callback specific to Nodes 2.0 rendering
      const callback = (v: unknown) => {
        const value = normalizeWidgetValue(v)
        widget.value = value ?? undefined
        // Match litegraph callback signature: (value, canvas, node, pos, event)
        // Some extensions (e.g., Impact Pack) expect node as the 3rd parameter
        widget.callback?.(value, app.canvas, node)
        // Trigger redraw for all legacy widgets on this node (e.g., mask preview)
        // This ensures widgets that depend on other widget values get updated
        node.widgets?.forEach((w) => w.triggerDraw?.())
      }

      const isPromotedPseudoWidget =
        isPromotedWidgetView(widget) && widget.sourceWidgetName.startsWith('$$')

      // Extract only render-critical options (canvasOnly, advanced, read_only)
      const options = extractWidgetDisplayOptions(widget)
      const subgraphId = node.isSubgraphNode() && node.subgraph.id

      const resolvedSourceResult =
        isPromotedWidgetView(widget) && promotedSource
          ? resolveConcretePromotedWidget(
              node,
              promotedSource.sourceNodeId,
              promotedSource.sourceWidgetName
            )
          : null
      const resolvedSource =
        resolvedSourceResult?.status === 'resolved'
          ? resolvedSourceResult.resolved
          : undefined
      const sourceWidget = resolvedSource?.widget
      const sourceNode = resolvedSource?.node

      const effectiveWidget = sourceWidget ?? widget

      const localId = isPromotedWidgetView(widget)
        ? String(sourceNode?.id ?? promotedSource?.sourceNodeId)
        : undefined
      const nodeId =
        subgraphId && localId ? `${subgraphId}:${localId}` : undefined
      const sourceWidgetName = isPromotedWidgetView(widget)
        ? (sourceWidget?.name ?? promotedSource?.sourceWidgetName)
        : undefined
      const name = sourceWidgetName ?? displayName

      if (isPromotedWidgetView(widget)) widget.ensureHostWidgetState()

      return {
        entityId: getWidgetEntityIdForNode(node, widget),
        nodeId,
        name,
        type: effectiveWidget.type,
        ...sharedEnhancements,
        callback,
        hasLayoutSize: typeof effectiveWidget.computeLayoutSize === 'function',
        isDOMWidget: isDOMWidget(widget) || isPromotedDOMWidget(widget),
        options: isPromotedPseudoWidget
          ? {
              ...(extractWidgetDisplayOptions(effectiveWidget) ?? options),
              canvasOnly: true
            }
          : (extractWidgetDisplayOptions(effectiveWidget) ?? options),
        slotMetadata: slotInfo,
        // For promoted widgets, name is sourceWidgetName while widget.name
        // is the subgraph input slot name — store the slot name for lookups.
        slotName: name !== widget.name ? widget.name : undefined,
        sourceExecutionId:
          sourceNode && app.rootGraph
            ? (getExecutionIdByNode(app.rootGraph, sourceNode) ?? undefined)
            : undefined,
        tooltip: widget.tooltip,
        promotedLabel: isPromotedWidgetView(widget) ? widget.label : undefined
      }
    } catch (error) {
      console.warn(
        '[safeWidgetMapper] Failed to map widget:',
        widget.name,
        error
      )
      return {
        name: widget.name || 'unknown',
        type: widget.type || 'text'
      }
    }
  }
}

function buildSlotMetadata(
  inputs: INodeInputSlot[] | undefined,
  graphRef: LGraph | null | undefined
): Map<string, WidgetSlotMetadata> {
  const metadata = new Map<string, WidgetSlotMetadata>()
  inputs?.forEach((input, index) => {
    let originNodeId: string | undefined
    let originOutputName: string | undefined

    if (input.link != null && graphRef) {
      const link = graphRef.getLink(input.link)
      const originNode = link ? graphRef.getNodeById(link.origin_id) : null
      if (link && originNode) {
        originNodeId = String(link.origin_id)
        originOutputName = originNode.outputs?.[link.origin_slot]?.name
      }
    }

    const slotInfo: WidgetSlotMetadata = {
      index,
      linked: input.link != null,
      originNodeId,
      originOutputName,
      type: String(input.type)
    }
    if (input.name) metadata.set(input.name, slotInfo)
    if (input.widget?.name) metadata.set(input.widget.name, slotInfo)
  })
  return metadata
}

// Extract safe data from LiteGraph node for Vue consumption
export function extractVueNodeData(node: LGraphNode): VueNodeData {
  // Determine subgraph ID - null for root graph, string for subgraphs
  const subgraphId =
    node.graph && 'id' in node.graph && node.graph !== node.graph.rootGraph
      ? String(node.graph.id)
      : null
  // Extract safe widget data
  const slotMetadata = new Map<string, WidgetSlotMetadata>()

  const existingWidgetsDescriptor = Object.getOwnPropertyDescriptor(
    node,
    'widgets'
  )
  const reactiveWidgets = shallowReactive<IBaseWidget[]>(node.widgets ?? [])
  if (existingWidgetsDescriptor?.get) {
    // Node has a custom widgets getter (e.g. SubgraphNode's synthetic getter).
    // Preserve it but sync results into a reactive array for Vue.
    const originalGetter = existingWidgetsDescriptor.get
    Object.defineProperty(node, 'widgets', {
      get() {
        const current: IBaseWidget[] = originalGetter.call(node) ?? []
        if (
          current.length !== reactiveWidgets.length ||
          current.some((w, i) => w !== reactiveWidgets[i])
        ) {
          reactiveWidgets.splice(0, reactiveWidgets.length, ...current)
        }
        return reactiveWidgets
      },
      set: existingWidgetsDescriptor.set ?? (() => {}),
      configurable: true,
      enumerable: true
    })
  } else {
    Object.defineProperty(node, 'widgets', {
      get() {
        return reactiveWidgets
      },
      set(v) {
        reactiveWidgets.splice(0, reactiveWidgets.length, ...v)
      },
      configurable: true,
      enumerable: true
    })
  }
  const reactiveInputs = shallowReactive<INodeInputSlot[]>(node.inputs ?? [])
  Object.defineProperty(node, 'inputs', {
    get() {
      return reactiveInputs
    },
    set(v) {
      reactiveInputs.splice(0, reactiveInputs.length, ...v)
    },
    configurable: true,
    enumerable: true
  })
  const reactiveOutputs = shallowReactive<INodeOutputSlot[]>(node.outputs ?? [])
  Object.defineProperty(node, 'outputs', {
    get() {
      return reactiveOutputs
    },
    set(v) {
      reactiveOutputs.splice(0, reactiveOutputs.length, ...v)
    },
    configurable: true,
    enumerable: true
  })

  const safeWidgets = reactiveComputed<SafeWidgetData[]>(() => {
    const widgetsSnapshot = node.widgets ?? []

    const freshMetadata = buildSlotMetadata(node.inputs, node.graph)
    slotMetadata.clear()
    for (const [key, value] of freshMetadata) {
      slotMetadata.set(key, value)
    }
    return widgetsSnapshot.map(safeWidgetMapper(node, slotMetadata))
  })

  const nodeType =
    node.type ||
    node.constructor?.comfyClass ||
    node.constructor?.title ||
    node.constructor?.name ||
    'Unknown'

  const apiNode = node.constructor?.nodeData?.api_node ?? false
  const badges = node.badges

  return {
    id: String(node.id),
    title: typeof node.title === 'string' ? node.title : '',
    type: nodeType,
    mode: node.mode || 0,
    titleMode: node.title_mode,
    selected: node.selected || false,
    executing: false, // Will be updated separately based on execution state
    subgraphId,
    apiNode,
    badges,
    hasErrors: !!node.has_errors,
    widgets: safeWidgets,
    inputs: reactiveInputs,
    outputs: reactiveOutputs,
    flags: node.flags ? { ...node.flags } : undefined,
    color: node.color || undefined,
    bgcolor: node.bgcolor || undefined,
    resizable: node.resizable,
    shape: node.shape,
    showAdvanced: node.showAdvanced
  }
}

export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
  // Get layout mutations composable
  const { createNode, deleteNode, setSource } = useLayoutMutations()
  // Safe reactive data extracted from LiteGraph nodes
  const vueNodeData = reactive(new Map<string, VueNodeData>())

  // Non-reactive storage for original LiteGraph nodes
  const nodeRefs = new Map<string, LGraphNode>()

  const refreshNodeSlots = (nodeId: string) => {
    const nodeRef = nodeRefs.get(nodeId)
    const currentData = vueNodeData.get(nodeId)

    if (!nodeRef || !currentData) return

    const slotMetadata = buildSlotMetadata(nodeRef.inputs, graph)

    // Update only widgets with new slot metadata, keeping other widget data intact
    for (const widget of currentData.widgets ?? []) {
      widget.slotMetadata = slotMetadata.get(widget.slotName ?? widget.name)
    }
  }

  // Get access to original LiteGraph node (non-reactive)
  const getNode = (id: string): LGraphNode | undefined => {
    return nodeRefs.get(id)
  }

  const syncWithGraph = () => {
    if (!graph?._nodes) return

    const currentNodes = new Set(graph._nodes.map((n) => String(n.id)))

    // Remove deleted nodes
    for (const id of Array.from(vueNodeData.keys())) {
      if (!currentNodes.has(id)) {
        nodeRefs.delete(id)
        vueNodeData.delete(id)
      }
    }

    // Add/update existing nodes
    graph._nodes.forEach((node) => {
      const id = String(node.id)

      // Store non-reactive reference
      nodeRefs.set(id, node)

      // Extract and store safe data for Vue
      vueNodeData.set(id, extractVueNodeData(node))
    })
  }

  /**
   * Handles node addition to the graph - sets up Vue state and spatial indexing
   * Defers position extraction until after potential configure() calls
   */
  const handleNodeAdded = (
    node: LGraphNode,
    originalCallback?: (node: LGraphNode) => void
  ) => {
    const id = String(node.id)

    // Store non-reactive reference to original node
    nodeRefs.set(id, node)

    // Extract initial data for Vue (may be incomplete during graph configure)
    vueNodeData.set(id, extractVueNodeData(node))

    const initializeVueNodeLayout = () => {
      // Check if the node was removed mid-sequence
      if (!nodeRefs.has(id)) return

      // Extract actual positions after configure() has potentially updated them
      const nodePosition = { x: node.pos[0], y: node.pos[1] }
      const nodeSize = { width: node.size[0], height: node.size[1] }

      // Skip layout creation if it already exists
      // (e.g. in-place node replacement where the old node's layout is reused for the new node with the same ID).
      const existingLayout = layoutStore.getNodeLayoutRef(id).value
      if (existingLayout) return

      // Add node to layout store with final positions
      setSource(LayoutSource.Canvas)
      void createNode(id, {
        position: nodePosition,
        size: nodeSize,
        zIndex: node.order || 0,
        visible: true
      })
    }

    // Check if we're in the middle of configuring the graph (workflow loading)
    if (window.app?.configuringGraph) {
      // During workflow loading - defer layout initialization until configure completes
      // Chain our callback with any existing onAfterGraphConfigured callback
      node.onAfterGraphConfigured = useChainCallback(
        node.onAfterGraphConfigured,
        () => {
          // Re-extract data now that configure() has populated title/slots/widgets/etc.
          vueNodeData.set(id, extractVueNodeData(node))
          initializeVueNodeLayout()
        }
      )
    } else {
      // Not during workflow loading - initialize layout immediately
      // This handles individual node additions during normal operation
      initializeVueNodeLayout()
    }

    // Call original callback if provided
    if (originalCallback) {
      void originalCallback(node)
    }
  }

  /**
   * Handles node removal from the graph - cleans up all references
   */
  const handleNodeRemoved = (
    node: LGraphNode,
    originalCallback?: (node: LGraphNode) => void
  ) => {
    const id = String(node.id)

    // Remove node from layout store
    setSource(LayoutSource.Canvas)
    void deleteNode(id)

    // Clean up all tracking references
    nodeRefs.delete(id)
    vueNodeData.delete(id)

    // Call original callback if provided
    if (originalCallback) {
      originalCallback(node)
    }
  }

  /**
   * Creates cleanup function for event listeners and state
   */
  const createCleanupFunction = (
    originalOnNodeAdded: ((node: LGraphNode) => void) | undefined,
    originalOnNodeRemoved: ((node: LGraphNode) => void) | undefined,
    originalOnTrigger: ((event: LGraphTriggerEvent) => void) | undefined
  ) => {
    return () => {
      // Restore original callbacks
      graph.onNodeAdded = originalOnNodeAdded || undefined
      graph.onNodeRemoved = originalOnNodeRemoved || undefined
      graph.onTrigger = originalOnTrigger || undefined

      // Clear all state maps
      nodeRefs.clear()
      vueNodeData.clear()
    }
  }

  /**
   * Sets up event listeners - now simplified with extracted handlers
   */
  const setupEventListeners = (): (() => void) => {
    // Store original callbacks
    const originalOnNodeAdded = graph.onNodeAdded
    const originalOnNodeRemoved = graph.onNodeRemoved
    const originalOnTrigger = graph.onTrigger

    // Set up graph event handlers
    graph.onNodeAdded = (node: LGraphNode) => {
      handleNodeAdded(node, originalOnNodeAdded)
    }

    graph.onNodeRemoved = (node: LGraphNode) => {
      handleNodeRemoved(node, originalOnNodeRemoved)
    }

    const triggerHandlers: {
      [K in LGraphTriggerAction]: (event: LGraphTriggerParam<K>) => void
    } = {
      'node:property:changed': (propertyEvent) => {
        const nodeId = String(propertyEvent.nodeId)
        const currentData = vueNodeData.get(nodeId)

        if (currentData) {
          switch (propertyEvent.property) {
            case 'title':
              vueNodeData.set(nodeId, {
                ...currentData,
                title: String(propertyEvent.newValue)
              })
              break
            case 'has_errors':
              vueNodeData.set(nodeId, {
                ...currentData,
                hasErrors: Boolean(propertyEvent.newValue)
              })
              break
            case 'flags.collapsed':
              vueNodeData.set(nodeId, {
                ...currentData,
                flags: {
                  ...currentData.flags,
                  collapsed: Boolean(propertyEvent.newValue)
                }
              })
              break
            case 'flags.ghost':
              vueNodeData.set(nodeId, {
                ...currentData,
                flags: {
                  ...currentData.flags,
                  ghost: Boolean(propertyEvent.newValue)
                }
              })
              break
            case 'flags.pinned':
              vueNodeData.set(nodeId, {
                ...currentData,
                flags: {
                  ...currentData.flags,
                  pinned: Boolean(propertyEvent.newValue)
                }
              })
              break
            case 'mode':
              vueNodeData.set(nodeId, {
                ...currentData,
                mode:
                  typeof propertyEvent.newValue === 'number'
                    ? propertyEvent.newValue
                    : 0
              })
              break
            case 'color':
              vueNodeData.set(nodeId, {
                ...currentData,
                color:
                  typeof propertyEvent.newValue === 'string'
                    ? propertyEvent.newValue
                    : undefined
              })
              break
            case 'bgcolor':
              vueNodeData.set(nodeId, {
                ...currentData,
                bgcolor:
                  typeof propertyEvent.newValue === 'string'
                    ? propertyEvent.newValue
                    : undefined
              })
              break
            case 'shape':
              vueNodeData.set(nodeId, {
                ...currentData,
                shape:
                  typeof propertyEvent.newValue === 'number'
                    ? propertyEvent.newValue
                    : undefined
              })
              break
            case 'showAdvanced':
              vueNodeData.set(nodeId, {
                ...currentData,
                showAdvanced: Boolean(propertyEvent.newValue)
              })
              break
            case 'badges':
              vueNodeData.set(nodeId, {
                ...currentData,
                badges: propertyEvent.newValue as Badges
              })
              break
          }
        }
      },
      'node:slot-errors:changed': (slotErrorsEvent) => {
        refreshNodeSlots(String(slotErrorsEvent.nodeId))
      },
      'node:slot-links:changed': (slotLinksEvent) => {
        if (slotLinksEvent.slotType === NodeSlotType.INPUT) {
          refreshNodeSlots(String(slotLinksEvent.nodeId))
        }
      },
      'node:slot-label:changed': (slotLabelEvent) => {
        const nodeId = String(slotLabelEvent.nodeId)
        const nodeRef = nodeRefs.get(nodeId)
        if (!nodeRef) return

        // Force shallowReactive to detect the deep property change
        // by re-assigning the affected array through the defineProperty setter.
        if (slotLabelEvent.slotType !== NodeSlotType.OUTPUT && nodeRef.inputs) {
          nodeRef.inputs = [...nodeRef.inputs]
        }
        if (slotLabelEvent.slotType !== NodeSlotType.INPUT && nodeRef.outputs) {
          nodeRef.outputs = [...nodeRef.outputs]
        }
        // Re-extract widget data so promotedLabel reflects the rename
        vueNodeData.set(nodeId, extractVueNodeData(nodeRef))
      }
    }

    graph.onTrigger = (event: LGraphTriggerEvent) => {
      switch (event.type) {
        case 'node:property:changed':
          triggerHandlers['node:property:changed'](event)
          break
        case 'node:slot-errors:changed':
          triggerHandlers['node:slot-errors:changed'](event)
          break
        case 'node:slot-links:changed':
          triggerHandlers['node:slot-links:changed'](event)
          break
        case 'node:slot-label:changed':
          triggerHandlers['node:slot-label:changed'](event)
          break
      }

      // Chain to original handler
      originalOnTrigger?.(event)
    }

    // Initialize state
    syncWithGraph()

    // Return cleanup function
    return createCleanupFunction(
      originalOnNodeAdded || undefined,
      originalOnNodeRemoved || undefined,
      originalOnTrigger || undefined
    )
  }

  // Set up event listeners immediately
  const cleanup = setupEventListeners()

  // Process any existing nodes after event listeners are set up
  if (graph._nodes && graph._nodes.length > 0) {
    graph._nodes.forEach((node: LGraphNode) => {
      if (graph.onNodeAdded) {
        graph.onNodeAdded(node)
      }
    })
  }

  return {
    vueNodeData,
    getNode,
    cleanup
  }
}
