'use client'

import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import ReactFlow, {
  Background,
  BackgroundVariant,
  Controls,
  useNodesState,
  useEdgesState,
  useReactFlow,
  ReactFlowProvider,
  addEdge,
  Connection,
  Node,
  Edge,
  NodeTypes,
  EdgeTypes,
  MarkerType,
  NodeMouseHandler,
  EdgeMouseHandler,
} from 'reactflow'
import { nanoid } from 'nanoid'
import 'reactflow/dist/style.css'
import Toolbar from '@/components/editor/Toolbar'
import Toast from '@/components/Toast'
import { createClient } from '@/lib/supabase/client'
import DialogueNode from '@/components/editor/nodes/DialogueNode'
import ChoiceNode from '@/components/editor/nodes/ChoiceNode'
import VariableNode from '@/components/editor/nodes/VariableNode'
import ConditionalEdge from '@/components/editor/edges/ConditionalEdge'
import ContextMenu, { ContextMenuType } from '@/components/editor/ContextMenu'
import ProjectSettingsModal from '@/components/editor/ProjectSettingsModal'
import ConditionEditor from '@/components/editor/ConditionEditor'
import ExportValidationModal, { type ValidationIssue } from '@/components/editor/ExportValidationModal'
import { EditorProvider } from '@/components/editor/EditorContext'
import { RealtimeConnection, type ConnectionState, type PresenceUser, type RemoteCursor, type NodeLock } from '@/lib/collaboration/realtime'
import type { NodeLockInfo } from '@/components/editor/EditorContext'
import RemoteCursors from '@/components/editor/RemoteCursors'
import { CRDTManager } from '@/lib/collaboration/crdt'
import { AuditTrailRecorder } from '@/lib/collaboration/auditTrail'
import ShareModal from '@/components/editor/ShareModal'
import ActivityHistorySidebar, { type AuditEntry } from '@/components/editor/ActivityHistorySidebar'
import CollaborationToast, { type CollaborationNotification } from '@/components/editor/CollaborationToast'
import type { FlowchartData, FlowchartNode, FlowchartEdge, Character, Variable, Condition } from '@/types/flowchart'

// LocalStorage key prefix for draft saves
const DRAFT_KEY_PREFIX = 'vnwrite-draft-'

// Debounce delay in ms
const AUTOSAVE_DEBOUNCE_MS = 1000

type ContextMenuState = {
  x: number
  y: number
  type: ContextMenuType
  nodeId?: string
  edgeId?: string
} | null

type ConditionEditorState = {
  edgeId: string
  condition?: Condition
} | null

type FlowchartEditorProps = {
  projectId: string
  projectName: string
  userId: string
  userDisplayName: string
  isOwner: boolean
  initialData: FlowchartData
  needsMigration?: boolean
}

// Convert our FlowchartNode type to React Flow Node type
function toReactFlowNodes(nodes: FlowchartNode[]): Node[] {
  return nodes.map((node) => ({
    id: node.id,
    type: node.type,
    position: node.position,
    data: node.data,
  }))
}

// Convert our FlowchartEdge type to React Flow Edge type
function toReactFlowEdges(edges: FlowchartEdge[]): Edge[] {
  return edges.map((edge) => ({
    id: edge.id,
    source: edge.source,
    sourceHandle: edge.sourceHandle,
    target: edge.target,
    targetHandle: edge.targetHandle,
    data: edge.data,
    type: 'conditional',
    markerEnd: {
      type: MarkerType.ArrowClosed,
    },
  }))
}

// Convert React Flow Node type back to our FlowchartNode type
function fromReactFlowNodes(nodes: Node[]): FlowchartNode[] {
  return nodes.map((node) => ({
    id: node.id,
    type: node.type as 'dialogue' | 'choice' | 'variable',
    position: node.position,
    data: node.data,
  })) as FlowchartNode[]
}

// Convert React Flow Edge type back to our FlowchartEdge type
function fromReactFlowEdges(edges: Edge[]): FlowchartEdge[] {
  return edges.map((edge) => ({
    id: edge.id,
    source: edge.source,
    sourceHandle: edge.sourceHandle ?? undefined,
    target: edge.target,
    targetHandle: edge.targetHandle ?? undefined,
    data: edge.data,
  }))
}

// Get LocalStorage key for a project
function getDraftKey(projectId: string): string {
  return `${DRAFT_KEY_PREFIX}${projectId}`
}

// Save draft to LocalStorage
function saveDraft(projectId: string, data: FlowchartData): void {
  try {
    localStorage.setItem(getDraftKey(projectId), JSON.stringify(data))
  } catch (error) {
    console.error('Failed to save draft to LocalStorage:', error)
  }
}

// Load draft from LocalStorage
function loadDraft(projectId: string): FlowchartData | null {
  try {
    const draft = localStorage.getItem(getDraftKey(projectId))
    if (!draft) return null
    return JSON.parse(draft) as FlowchartData
  } catch (error) {
    console.error('Failed to load draft from LocalStorage:', error)
    return null
  }
}

// Clear draft from LocalStorage
export function clearDraft(projectId: string): void {
  try {
    localStorage.removeItem(getDraftKey(projectId))
  } catch (error) {
    console.error('Failed to clear draft from LocalStorage:', error)
  }
}

// Compare two FlowchartData objects for equality
function flowchartDataEquals(a: FlowchartData, b: FlowchartData): boolean {
  return JSON.stringify(a) === JSON.stringify(b)
}

// Validate imported flowchart data structure
function isValidFlowchartData(data: unknown): data is FlowchartData {
  if (!data || typeof data !== 'object') return false
  const obj = data as Record<string, unknown>
  if (!Array.isArray(obj.nodes)) return false
  if (!Array.isArray(obj.edges)) return false
  return true
}

// Find the first node (node with no incoming edges)
function findFirstNode(nodes: FlowchartNode[], edges: FlowchartEdge[]): FlowchartNode | null {
  const targetIds = new Set(edges.map((e) => e.target))
  const startNodes = nodes.filter((n) => !targetIds.has(n.id))
  return startNodes[0] || nodes[0] || null
}

// Get outgoing edge from a node
function getOutgoingEdge(nodeId: string, edges: FlowchartEdge[], sourceHandle?: string): FlowchartEdge | undefined {
  return edges.find((e) => e.source === nodeId && (sourceHandle === undefined || e.sourceHandle === sourceHandle))
}

// Get all outgoing edges from a node
function getOutgoingEdges(nodeId: string, edges: FlowchartEdge[]): FlowchartEdge[] {
  return edges.filter((e) => e.source === nodeId)
}

// Convert flowchart to Ren'Py JSON format
function convertToRenpyFormat(
  nodes: FlowchartNode[],
  edges: FlowchartEdge[],
  projectName: string
): { projectName: string; exportedAt: string; sections: Record<string, unknown[]> } {
  const nodeMap = new Map<string, FlowchartNode>(nodes.map((n) => [n.id, n]))
  const visited = new Set<string>()
  const sections: Record<string, unknown[]> = {}
  let currentSectionName = 'start'
  let currentSection: unknown[] = []

  const nodeLabels = new Map<string, string>()
  let labelCounter = 0

  function getNodeLabel(nodeId: string): string {
    if (!nodeLabels.has(nodeId)) {
      const node = nodeMap.get(nodeId)
      if (node?.type === 'dialogue' && node.data.speaker) {
        nodeLabels.set(nodeId, `section_${node.data.speaker.toLowerCase().replace(/\s+/g, '_')}_${labelCounter++}`)
      } else {
        nodeLabels.set(nodeId, `section_${labelCounter++}`)
      }
    }
    return nodeLabels.get(nodeId)!
  }

  function processNode(nodeId: string): void {
    if (visited.has(nodeId)) return
    visited.add(nodeId)

    const node = nodeMap.get(nodeId)
    if (!node) return

    if (node.type === 'dialogue') {
      const outgoingEdge = getOutgoingEdge(nodeId, edges)
      const renpyNode: Record<string, unknown> = {
        type: 'dialogue',
        speaker: node.data.speaker || '',
        text: node.data.text,
      }
      if (outgoingEdge) {
        renpyNode.next = visited.has(outgoingEdge.target) ? getNodeLabel(outgoingEdge.target) : outgoingEdge.target
        if (outgoingEdge.data?.condition) {
          renpyNode.condition = outgoingEdge.data.condition
        }
      }
      currentSection.push(renpyNode)
      if (outgoingEdge && !visited.has(outgoingEdge.target)) {
        processNode(outgoingEdge.target)
      }
    } else if (node.type === 'choice') {
      const outEdges = getOutgoingEdges(nodeId, edges)
      const choices = outEdges.map((edge) => {
        const choiceData: Record<string, unknown> = {
          label: edge.sourceHandle || 'Choice',
          next: edge.target,
        }
        if (edge.data?.condition) {
          choiceData.condition = edge.data.condition
        }
        return choiceData
      })
      currentSection.push({
        type: 'menu',
        prompt: node.data.prompt || '',
        choices,
      })
      // Process each choice target as a new section
      outEdges.forEach((edge) => {
        if (!visited.has(edge.target)) {
          sections[currentSectionName] = currentSection
          currentSectionName = getNodeLabel(edge.target)
          currentSection = []
          processNode(edge.target)
        }
      })
    } else if (node.type === 'variable') {
      const outgoingEdge = getOutgoingEdge(nodeId, edges)
      const renpyNode: Record<string, unknown> = {
        type: 'variable',
        name: node.data.variableName || '',
        operation: node.data.operation || 'set',
        value: node.data.value ?? 0,
      }
      if (outgoingEdge) {
        renpyNode.next = visited.has(outgoingEdge.target) ? getNodeLabel(outgoingEdge.target) : outgoingEdge.target
        if (outgoingEdge.data?.condition) {
          renpyNode.condition = outgoingEdge.data.condition
        }
      }
      currentSection.push(renpyNode)
      if (outgoingEdge && !visited.has(outgoingEdge.target)) {
        processNode(outgoingEdge.target)
      }
    }
  }

  const startNode = findFirstNode(nodes, edges)
  if (startNode) {
    processNode(startNode.id)
  }
  sections[currentSectionName] = currentSection

  return {
    projectName,
    exportedAt: new Date().toISOString(),
    sections,
  }
}

const RANDOM_COLORS = [
  '#EF4444', '#F97316', '#F59E0B', '#10B981',
  '#3B82F6', '#8B5CF6', '#EC4899', '#14B8A6',
  '#6366F1', '#F43F5E', '#84CC16', '#06B6D4',
]

function randomHexColor(): string {
  return RANDOM_COLORS[Math.floor(Math.random() * RANDOM_COLORS.length)]
}

// Generate a consistent color from a user ID hash (same logic as PresenceAvatars)
function getUserColor(userId: string): string {
  let hash = 0
  for (let i = 0; i < userId.length; i++) {
    hash = ((hash << 5) - hash + userId.charCodeAt(i)) | 0
  }
  return RANDOM_COLORS[Math.abs(hash) % RANDOM_COLORS.length]
}

// Determine the action type for a revert audit entry
function getRevertActionType(originalActionType: string): string {
  switch (originalActionType) {
    case 'node_add': return 'node_delete' // reverting an add = deleting
    case 'node_delete': return 'node_add' // reverting a delete = adding back
    case 'node_update': return 'node_update' // reverting an update = updating back
    case 'edge_add': return 'edge_delete'
    case 'edge_delete': return 'edge_add'
    case 'edge_update': return 'edge_update'
    default: return originalActionType
  }
}

// Compute auto-migration of existing free-text values to character/variable definitions
function computeMigration(initialData: FlowchartData, shouldMigrate: boolean) {
  if (!shouldMigrate) {
    return {
      characters: initialData.characters,
      variables: initialData.variables,
      nodes: initialData.nodes,
      edges: initialData.edges,
      toastMessage: null as string | null,
    }
  }

  // Collect unique speaker names from dialogue nodes
  const speakerNames = new Set<string>()
  initialData.nodes.forEach((node) => {
    if (node.type === 'dialogue' && node.data?.speaker) {
      speakerNames.add(node.data.speaker)
    }
  })

  // Create character definitions from unique speaker names
  const newCharacters: Character[] = []
  const speakerToCharacterId = new Map<string, string>()
  speakerNames.forEach((name) => {
    const id = nanoid()
    newCharacters.push({ id, name, color: randomHexColor() })
    speakerToCharacterId.set(name, id)
  })

  // Collect unique variable names from variable nodes, edge conditions, and choice option conditions
  const variableNames = new Set<string>()
  initialData.nodes.forEach((node) => {
    if (node.type === 'variable' && node.data.variableName) {
      variableNames.add(node.data.variableName)
    }
    if (node.type === 'choice' && node.data.options) {
      node.data.options.forEach((opt) => {
        if (opt.condition?.variableName) {
          variableNames.add(opt.condition.variableName)
        }
      })
    }
  })
  initialData.edges.forEach((edge) => {
    if (edge.data?.condition?.variableName) {
      variableNames.add(edge.data.condition.variableName)
    }
  })

  // Create variable definitions from unique variable names
  const newVariables: Variable[] = []
  const varNameToId = new Map<string, string>()
  variableNames.forEach((name) => {
    const id = nanoid()
    newVariables.push({ id, name, type: 'numeric', initialValue: 0 })
    varNameToId.set(name, id)
  })

  // If nothing to migrate, return original data
  if (newCharacters.length === 0 && newVariables.length === 0) {
    return {
      characters: initialData.characters,
      variables: initialData.variables,
      nodes: initialData.nodes,
      edges: initialData.edges,
      toastMessage: null as string | null,
    }
  }

  // Update nodes with characterId/variableId references
  const migratedNodes = initialData.nodes.map((node) => {
    if (node.type === 'dialogue' && node.data.speaker) {
      const characterId = speakerToCharacterId.get(node.data.speaker)
      if (characterId) {
        return { ...node, data: { ...node.data, characterId } }
      }
    }
    if (node.type === 'variable' && node.data.variableName) {
      const variableId = varNameToId.get(node.data.variableName)
      if (variableId) {
        return { ...node, data: { ...node.data, variableId } }
      }
    }
    if (node.type === 'choice' && node.data.options) {
      const updatedOptions = node.data.options.map((opt) => {
        if (opt.condition?.variableName) {
          const variableId = varNameToId.get(opt.condition.variableName)
          if (variableId) {
            return { ...opt, condition: { ...opt.condition, variableId } }
          }
        }
        return opt
      })
      return { ...node, data: { ...node.data, options: updatedOptions } }
    }
    return node
  }) as typeof initialData.nodes

  // Update edges with variableId references
  const migratedEdges = initialData.edges.map((edge) => {
    if (edge.data?.condition?.variableName) {
      const variableId = varNameToId.get(edge.data.condition.variableName)
      if (variableId) {
        return {
          ...edge,
          data: { ...edge.data, condition: { ...edge.data.condition, variableId } },
        }
      }
    }
    return edge
  })

  // Build toast message
  const parts: string[] = []
  if (newCharacters.length > 0) {
    parts.push(`${newCharacters.length} character${newCharacters.length > 1 ? 's' : ''}`)
  }
  if (newVariables.length > 0) {
    parts.push(`${newVariables.length} variable${newVariables.length > 1 ? 's' : ''}`)
  }

  return {
    characters: newCharacters,
    variables: newVariables,
    nodes: migratedNodes,
    edges: migratedEdges,
    toastMessage: `Auto-imported ${parts.join(' and ')} from existing data`,
  }
}

// Inner component that uses useReactFlow hook
function FlowchartEditorInner({ projectId, projectName, userId, userDisplayName, isOwner, initialData, needsMigration }: FlowchartEditorProps) {
  // Define custom node types - memoized to prevent re-renders
  const nodeTypes: NodeTypes = useMemo(
    () => ({
      dialogue: DialogueNode,
      choice: ChoiceNode,
      variable: VariableNode,
    }),
    []
  )

  // Define custom edge types - memoized to prevent re-renders
  const edgeTypes: EdgeTypes = useMemo(
    () => ({
      conditional: ConditionalEdge,
    }),
    []
  )

  const { getViewport, screenToFlowPosition } = useReactFlow()

  const [contextMenu, setContextMenu] = useState<ContextMenuState>(null)
  const [conditionEditor, setConditionEditor] = useState<ConditionEditorState>(null)
  const [isSaving, setIsSaving] = useState(false)
  const [toast, setToast] = useState<{ message: string; type: 'success' | 'error'; action?: { label: string; onClick: () => void } } | null>(null)
  const [importConfirmDialog, setImportConfirmDialog] = useState<{
    pendingData: FlowchartData
  } | null>(null)
  const [showNavigationWarning, setShowNavigationWarning] = useState(false)

  // Track the last saved data to determine dirty state
  const lastSavedDataRef = useRef<FlowchartData>(initialData)

  // Ref for hidden file input
  const fileInputRef = useRef<HTMLInputElement>(null)

  // Ref for save function to enable retry without circular dependency
  const handleSaveRef = useRef<() => void>(() => {})

  // Check for saved draft on initial render (lazy initialization)
  const [draftState, setDraftState] = useState<{
    showPrompt: boolean
    savedDraft: FlowchartData | null
  }>(() => {
    // This runs only once on initial render (client-side)
    if (typeof window === 'undefined') {
      return { showPrompt: false, savedDraft: null }
    }
    const draft = loadDraft(projectId)
    if (draft && !flowchartDataEquals(draft, initialData)) {
      return { showPrompt: true, savedDraft: draft }
    }
    return { showPrompt: false, savedDraft: null }
  })

  // Compute migrated data once on first render using a lazy state initializer
  const [migratedData] = useState(() => computeMigration(initialData, !!needsMigration))

  const [nodes, setNodes, onNodesChange] = useNodesState(
    toReactFlowNodes(migratedData.nodes)
  )
  const [edges, setEdges, onEdgesChange] = useEdgesState(
    toReactFlowEdges(migratedData.edges)
  )

  const [characters, setCharacters] = useState<Character[]>(migratedData.characters)
  const [variables, setVariables] = useState<Variable[]>(migratedData.variables)

  // Refs to always have the latest characters/variables for the CRDT persist callback
  const charactersRef = useRef(characters)
  const variablesRef = useRef(variables)
  charactersRef.current = characters
  variablesRef.current = variables
  const [showSettings, setShowSettings] = useState(false)
  const [showShare, setShowShare] = useState(false)
  const [showHistory, setShowHistory] = useState(false)
  const [selectedEdgeId, setSelectedEdgeId] = useState<string | null>(null)
  const [toastMessage, setToastMessage] = useState<string | null>(migratedData.toastMessage)
  const [validationIssues, setValidationIssues] = useState<ValidationIssue[] | null>(null)
  const [warningNodeIds, setWarningNodeIds] = useState<Set<string>>(new Set())
  const [connectionState, setConnectionState] = useState<ConnectionState>('disconnected')
  const [presenceUsers, setPresenceUsers] = useState<PresenceUser[]>([])
  const [remoteCursors, setRemoteCursors] = useState<RemoteCursor[]>([])
  const realtimeRef = useRef<RealtimeConnection | null>(null)
  const crdtRef = useRef<CRDTManager | null>(null)
  const auditRef = useRef<AuditTrailRecorder | null>(null)
  const cursorThrottleRef = useRef<number>(0)
  const [collaborationNotifications, setCollaborationNotifications] = useState<CollaborationNotification[]>([])
  const [nodeLocks, setNodeLocks] = useState<Map<string, NodeLockInfo>>(new Map())
  const localLockRef = useRef<string | null>(null) // node ID currently locked by this user
  const lockExpiryTimerRef = useRef<ReturnType<typeof setInterval> | null>(null)
  const isRevertingRef = useRef(false) // guards against double audit recording during revert

  // Initialize CRDT manager and connect to Supabase Realtime channel on mount
  useEffect(() => {
    const supabase = createClient()

    const crdtManager = new CRDTManager({
      onNodesChange: (crdtNodes: FlowchartNode[]) => {
        setNodes(toReactFlowNodes(crdtNodes))
      },
      onEdgesChange: (crdtEdges: FlowchartEdge[]) => {
        setEdges(toReactFlowEdges(crdtEdges))
      },
      onPersist: async (persistNodes: FlowchartNode[], persistEdges: FlowchartEdge[]) => {
        try {
          // Auto-persist saves the current state for durability. We do NOT
          // broadcast state-refresh here because CRDT already syncs nodes/edges
          // via yjs-update broadcasts. Broadcasting here causes ping-pong:
          // other clients fetch from DB, overwrite their local variables/characters,
          // then their persist writes stale data back, causing a loop.
          await supabase
            .from('projects')
            .update({
              flowchart_data: {
                nodes: persistNodes,
                edges: persistEdges,
                characters: charactersRef.current,
                variables: variablesRef.current,
              },
            })
            .eq('id', projectId)
        } catch {
          // Persistence failure is non-critical; will retry on next change
        }
      },
    })

    // Initialize CRDT document from initial data
    crdtManager.initializeFromData(migratedData.nodes, migratedData.edges)
    crdtRef.current = crdtManager

    // Initialize audit trail recorder
    const auditRecorder = new AuditTrailRecorder(projectId, userId)
    auditRecorder.initialize(migratedData.nodes, migratedData.edges)
    auditRef.current = auditRecorder

    const connection = new RealtimeConnection(projectId, userId, userDisplayName, {
      onConnectionStateChange: setConnectionState,
      onPresenceSync: setPresenceUsers,
      onPresenceJoin: (user) => {
        setCollaborationNotifications((prev) => [
          ...prev,
          { id: nanoid(), displayName: user.displayName, type: 'join', color: getUserColor(user.userId) },
        ])
      },
      onPresenceLeave: (user) => {
        setCollaborationNotifications((prev) => [
          ...prev,
          { id: nanoid(), displayName: user.displayName, type: 'leave', color: getUserColor(user.userId) },
        ])
      },
      onCursorUpdate: (cursor) => {
        setRemoteCursors((prev) => {
          const existing = prev.findIndex((c) => c.userId === cursor.userId)
          if (existing >= 0) {
            const updated = [...prev]
            updated[existing] = cursor
            return updated
          }
          return [...prev, cursor]
        })
      },
      onNodeLockUpdate: (lock: NodeLock | null, lockUserId: string) => {
        setNodeLocks((prev) => {
          const next = new Map(prev)
          if (lock) {
            next.set(lock.nodeId, { ...lock, color: getUserColor(lock.userId) })
          } else {
            // Remove any lock held by this user
            for (const [nodeId, info] of next) {
              if (info.userId === lockUserId) {
                next.delete(nodeId)
              }
            }
          }
          return next
        })
      },
      onCRDTUpdate: (update: number[]) => {
        crdtManager.applyRemoteUpdate(update)
      },
      onStateRefresh: async () => {
        try {
          const sb = createClient()
          const { data } = await sb
            .from('projects')
            .select('flowchart_data')
            .eq('id', projectId)
            .single()
          if (data?.flowchart_data) {
            const fd = data.flowchart_data as FlowchartData
            setNodes(toReactFlowNodes(fd.nodes))
            setEdges(toReactFlowEdges(fd.edges))
            setCharacters(fd.characters)
            setVariables(fd.variables)
            crdtManager.refreshFromData(fd.nodes, fd.edges)
          }
        } catch {
          // Non-critical: user can still manually refresh
        }
      },
      onChannelSubscribed: (channel) => {
        crdtManager.connectChannel(channel)
      },
    })
    realtimeRef.current = connection
    connection.connect()

    return () => {
      // Release any held lock before disconnecting
      if (localLockRef.current) {
        connection.broadcastNodeLock(null)
        localLockRef.current = null
      }
      connection.disconnect()
      realtimeRef.current = null
      crdtManager.destroy()
      crdtRef.current = null
      auditRecorder.destroy()
      auditRef.current = null
    }
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [projectId, userId, userDisplayName])

  // Manage connection lifecycle based on visibility and user activity
  useEffect(() => {
    const handleVisibilityChange = () => {
      if (!document.hidden) {
        realtimeRef.current?.notifyActivity()
      }
    }

    // Throttle activity notifications to avoid excessive calls
    let activityThrottled = false
    const throttledActivity = () => {
      if (activityThrottled) return
      activityThrottled = true
      realtimeRef.current?.notifyActivity()
      setTimeout(() => { activityThrottled = false }, 10_000)
    }

    document.addEventListener('visibilitychange', handleVisibilityChange)
    document.addEventListener('mousedown', throttledActivity)
    document.addEventListener('keydown', throttledActivity)
    document.addEventListener('scroll', throttledActivity, true)
    document.addEventListener('mousemove', throttledActivity)

    return () => {
      document.removeEventListener('visibilitychange', handleVisibilityChange)
      document.removeEventListener('mousedown', throttledActivity)
      document.removeEventListener('keydown', throttledActivity)
      document.removeEventListener('scroll', throttledActivity, true)
      document.removeEventListener('mousemove', throttledActivity)
    }
  }, [])

  // Sync local React Flow state changes to CRDT (skip remote-originated updates)
  const nodesForCRDT = useMemo(() => {
    return nodes.map((node) => ({
      id: node.id,
      type: node.type as 'dialogue' | 'choice' | 'variable',
      position: node.position,
      data: node.data,
    })) as FlowchartNode[]
  }, [nodes])

  const edgesForCRDT = useMemo(() => {
    return edges.map((edge) => ({
      id: edge.id,
      source: edge.source,
      sourceHandle: edge.sourceHandle,
      target: edge.target,
      targetHandle: edge.targetHandle,
      data: edge.data,
    })) as FlowchartEdge[]
  }, [edges])

  useEffect(() => {
    crdtRef.current?.updateNodes(nodesForCRDT)
    if (!isRevertingRef.current) {
      auditRef.current?.recordNodeChanges(nodesForCRDT)
    }
  }, [nodesForCRDT])

  useEffect(() => {
    crdtRef.current?.updateEdges(edgesForCRDT)
    if (!isRevertingRef.current) {
      auditRef.current?.recordEdgeChanges(edgesForCRDT)
    }
  }, [edgesForCRDT])

  // Broadcast cursor position on mouse move (throttled to 50ms)
  const handleMouseMove = useCallback(
    (event: React.MouseEvent) => {
      const now = Date.now()
      if (now - cursorThrottleRef.current < 50) return
      cursorThrottleRef.current = now

      const connection = realtimeRef.current
      if (!connection) return

      // Convert screen position to flow coordinates
      const bounds = event.currentTarget.getBoundingClientRect()
      const screenX = event.clientX - bounds.left
      const screenY = event.clientY - bounds.top
      const flowPosition = screenToFlowPosition({ x: screenX, y: screenY })

      connection.broadcastCursor(flowPosition)
    },
    [screenToFlowPosition]
  )

  // Remove cursors and locks for users who leave
  useEffect(() => {
    setRemoteCursors((prev) =>
      prev.filter((cursor) =>
        presenceUsers.some((u) => u.userId === cursor.userId)
      )
    )
    setNodeLocks((prev) => {
      const next = new Map(prev)
      let changed = false
      for (const [nodeId, info] of next) {
        if (!presenceUsers.some((u) => u.userId === info.userId)) {
          next.delete(nodeId)
          changed = true
        }
      }
      return changed ? next : prev
    })
  }, [presenceUsers])

  // Lock auto-expiry: expire locks after 60 seconds of inactivity
  useEffect(() => {
    const LOCK_EXPIRY_MS = 60_000
    lockExpiryTimerRef.current = setInterval(() => {
      const now = Date.now()
      setNodeLocks((prev) => {
        const next = new Map(prev)
        let changed = false
        for (const [nodeId, info] of next) {
          if (now - info.lockedAt > LOCK_EXPIRY_MS) {
            next.delete(nodeId)
            changed = true
          }
        }
        return changed ? next : prev
      })
    }, 5000) // Check every 5 seconds

    return () => {
      if (lockExpiryTimerRef.current) {
        clearInterval(lockExpiryTimerRef.current)
        lockExpiryTimerRef.current = null
      }
    }
  }, [])

  const handleDismissNotification = useCallback((id: string) => {
    setCollaborationNotifications((prev) => prev.filter((n) => n.id !== id))
  }, [])

  const handleHistorySelectEntity = useCallback((entityId: string, actionType: string) => {
    if (actionType.startsWith('node_')) {
      // Select the node on the canvas
      setNodes((nds) =>
        nds.map((n) => ({ ...n, selected: n.id === entityId }))
      )
      setEdges((eds) =>
        eds.map((e) => ({ ...e, selected: false }))
      )
    } else if (actionType.startsWith('edge_')) {
      // Select the edge on the canvas
      setEdges((eds) =>
        eds.map((e) => ({ ...e, selected: e.id === entityId }))
      )
      setNodes((nds) =>
        nds.map((n) => ({ ...n, selected: false }))
      )
    }
  }, [setNodes, setEdges])

  const handleRevertEntry = useCallback((entry: AuditEntry) => {
    const { action_type, entity_id, previous_state, new_state } = entry

    // Guard against double audit recording - we write our own audit entry for the revert
    isRevertingRef.current = true
    // Reset after React has processed the state updates
    setTimeout(() => { isRevertingRef.current = false }, 0)

    if (action_type === 'node_add') {
      // Revert an addition = delete the node
      setNodes((nds) => nds.filter((n) => n.id !== entity_id))
      // Also remove any edges connected to this node
      setEdges((eds) => eds.filter((e) => e.source !== entity_id && e.target !== entity_id))
    } else if (action_type === 'node_update') {
      // Revert an update = restore previous state
      if (previous_state) {
        const prevNode = previous_state as unknown as FlowchartNode
        setNodes((nds) =>
          nds.map((n) =>
            n.id === entity_id
              ? { ...n, position: prevNode.position, data: prevNode.data }
              : n
          )
        )
      }
    } else if (action_type === 'node_delete') {
      // Revert a deletion = re-create the node from previous_state
      if (previous_state) {
        const prevNode = previous_state as unknown as FlowchartNode
        const newReactFlowNode: Node = {
          id: prevNode.id,
          type: prevNode.type,
          position: prevNode.position,
          data: prevNode.data,
        }
        setNodes((nds) => [...nds, newReactFlowNode])
      }
    } else if (action_type === 'edge_add') {
      // Revert an edge addition = delete the edge
      setEdges((eds) => eds.filter((e) => e.id !== entity_id))
    } else if (action_type === 'edge_update') {
      // Revert an edge update = restore previous state
      if (previous_state) {
        const prevEdge = previous_state as unknown as FlowchartEdge
        setEdges((eds) =>
          eds.map((e) =>
            e.id === entity_id
              ? {
                  ...e,
                  source: prevEdge.source,
                  sourceHandle: prevEdge.sourceHandle,
                  target: prevEdge.target,
                  targetHandle: prevEdge.targetHandle,
                  data: prevEdge.data,
                }
              : e
          )
        )
      }
    } else if (action_type === 'edge_delete') {
      // Revert an edge deletion = re-create the edge from previous_state
      if (previous_state) {
        const prevEdge = previous_state as unknown as FlowchartEdge
        const newReactFlowEdge: Edge = {
          id: prevEdge.id,
          source: prevEdge.source,
          sourceHandle: prevEdge.sourceHandle,
          target: prevEdge.target,
          targetHandle: prevEdge.targetHandle,
          data: prevEdge.data,
          type: 'conditional',
          markerEnd: { type: MarkerType.ArrowClosed },
        }
        setEdges((eds) => [...eds, newReactFlowEdge])
      }
    }

    // Record the revert as a new audit trail entry (fire-and-forget)
    const supabase = createClient()
    const revertActionType = getRevertActionType(action_type)
    supabase
      .from('audit_trail')
      .insert({
        project_id: projectId,
        user_id: userId,
        action_type: revertActionType,
        entity_id,
        previous_state: new_state, // "previous" for the revert is the current state (which was new_state)
        new_state: previous_state, // "new" for the revert is what we're restoring to
      })
      .then(({ error }) => {
        if (error) {
          console.error('[Revert] Failed to write revert audit entry:', error)
        }
      })

    setToast({ message: 'Change reverted successfully', type: 'success' })
  }, [setNodes, setEdges, projectId, userId])

  const handleNodeFocus = useCallback((nodeId: string) => {
    // Broadcast lock for this node
    localLockRef.current = nodeId
    realtimeRef.current?.broadcastNodeLock(nodeId)
  }, [])

  const handleNodeBlur = useCallback(() => {
    // Release lock
    if (localLockRef.current) {
      localLockRef.current = null
      realtimeRef.current?.broadcastNodeLock(null)
    }
  }, [])

  const handleAddCharacter = useCallback(
    (name: string, color: string): string => {
      const id = nanoid()
      const newCharacter: Character = { id, name, color }
      setCharacters((prev) => [...prev, newCharacter])
      return id
    },
    []
  )

  const handleAddVariableDefinition = useCallback(
    (name: string, type: 'numeric' | 'string' | 'boolean', initialValue: number | string | boolean): string => {
      const id = nanoid()
      const newVariable: Variable = { id, name, type, initialValue }
      setVariables((prev) => [...prev, newVariable])
      return id
    },
    []
  )

  const editorContextValue = useMemo(
    () => ({
      characters,
      onAddCharacter: handleAddCharacter,
      variables,
      onAddVariable: handleAddVariableDefinition,
      nodeLocks,
      onNodeFocus: handleNodeFocus,
      onNodeBlur: handleNodeBlur,
    }),
    [characters, handleAddCharacter, variables, handleAddVariableDefinition, nodeLocks, handleNodeFocus, handleNodeBlur]
  )

  const getCharacterUsageCount = useCallback(
    (characterId: string) => {
      return nodes.filter((n) => n.type === 'dialogue' && n.data?.characterId === characterId).length
    },
    [nodes]
  )

  const getVariableUsageCount = useCallback(
    (variableId: string) => {
      const nodeCount = nodes.filter(
        (n) => n.type === 'variable' && n.data?.variableId === variableId
      ).length
      const edgeCount = edges.filter(
        (e) => e.data?.condition?.variableId === variableId
      ).length
      const choiceOptionCount = nodes.filter(
        (n) => n.type === 'choice'
      ).reduce((count, n) => {
        const options = n.data?.options || []
        return count + options.filter(
          (opt: { condition?: { variableId?: string } }) => opt.condition?.variableId === variableId
        ).length
      }, 0)
      return nodeCount + edgeCount + choiceOptionCount
    },
    [nodes, edges]
  )

  // Track debounce timer
  const saveTimerRef = useRef<NodeJS.Timeout | null>(null)

  // Debounced auto-save to LocalStorage
  useEffect(() => {
    // Don't save while draft prompt is showing
    if (draftState.showPrompt) return

    // Clear existing timer
    if (saveTimerRef.current) {
      clearTimeout(saveTimerRef.current)
    }

    // Set new timer
    saveTimerRef.current = setTimeout(() => {
      const currentData: FlowchartData = {
        nodes: fromReactFlowNodes(nodes),
        edges: fromReactFlowEdges(edges),
        characters,
        variables,
      }
      saveDraft(projectId, currentData)
    }, AUTOSAVE_DEBOUNCE_MS)

    // Cleanup on unmount
    return () => {
      if (saveTimerRef.current) {
        clearTimeout(saveTimerRef.current)
      }
    }
  }, [nodes, edges, characters, variables, projectId, draftState.showPrompt])

  // Calculate dirty state by comparing current data with last saved data
  const isDirty = useMemo(() => {
    const currentData: FlowchartData = {
      nodes: fromReactFlowNodes(nodes),
      edges: fromReactFlowEdges(edges),
      characters,
      variables,
    }
    return !flowchartDataEquals(currentData, lastSavedDataRef.current)
  }, [nodes, edges, characters, variables])

  // Browser beforeunload warning when dirty
  useEffect(() => {
    const handleBeforeUnload = (event: BeforeUnloadEvent) => {
      if (isDirty) {
        event.preventDefault()
        // Modern browsers require returnValue to be set
        event.returnValue = ''
        return ''
      }
    }

    window.addEventListener('beforeunload', handleBeforeUnload)

    return () => {
      window.removeEventListener('beforeunload', handleBeforeUnload)
    }
  }, [isDirty])

  // Handle restoring draft
  const handleRestoreDraft = useCallback(() => {
    if (draftState.savedDraft) {
      setNodes(toReactFlowNodes(draftState.savedDraft.nodes))
      setEdges(toReactFlowEdges(draftState.savedDraft.edges))
    }
    setDraftState({ showPrompt: false, savedDraft: null })
  }, [draftState.savedDraft, setNodes, setEdges])

  // Handle discarding draft
  const handleDiscardDraft = useCallback(() => {
    clearDraft(projectId)
    setDraftState({ showPrompt: false, savedDraft: null })
  }, [projectId])

  const onConnect = useCallback(
    (params: Connection) => {
      if (!params.source || !params.target) return
      const newEdge: Edge = {
        id: nanoid(),
        source: params.source,
        target: params.target,
        sourceHandle: params.sourceHandle,
        targetHandle: params.targetHandle,
        type: 'conditional',
        markerEnd: {
          type: MarkerType.ArrowClosed,
        },
      }
      setEdges((eds) => addEdge(newEdge, eds))
    },
    [setEdges]
  )

  // Get center position of current viewport for placing new nodes
  const getViewportCenter = useCallback(() => {
    const viewport = getViewport()
    // Calculate center based on viewport dimensions (assume ~800x600 visible area)
    // Adjust based on zoom level
    const centerX = (-viewport.x + 400) / viewport.zoom
    const centerY = (-viewport.y + 300) / viewport.zoom
    return { x: centerX, y: centerY }
  }, [getViewport])

  // Add dialogue node at viewport center
  const handleAddDialogue = useCallback(() => {
    const position = getViewportCenter()
    const newNode: Node = {
      id: nanoid(),
      type: 'dialogue',
      position,
      data: { speaker: '', text: '' },
    }
    setNodes((nodes) => [...nodes, newNode])
  }, [getViewportCenter, setNodes])

  const handleAddChoice = useCallback(() => {
    const position = getViewportCenter()
    const newNode: Node = {
      id: nanoid(),
      type: 'choice',
      position,
      data: {
        prompt: '',
        options: [
          { id: nanoid(), label: '' },
          { id: nanoid(), label: '' },
        ],
      },
    }
    setNodes((nodes) => [...nodes, newNode])
  }, [getViewportCenter, setNodes])

  const handleAddVariable = useCallback(() => {
    const position = getViewportCenter()
    const newNode: Node = {
      id: nanoid(),
      type: 'variable',
      position,
      data: {
        variableName: '',
        operation: 'set',
        value: 0,
      },
    }
    setNodes((nodes) => [...nodes, newNode])
  }, [getViewportCenter, setNodes])

  const handleSave = useCallback(async () => {
    if (isSaving) return

    setIsSaving(true)

    try {
      const supabase = createClient()

      // Convert React Flow state to FlowchartData
      const flowchartData: FlowchartData = {
        nodes: fromReactFlowNodes(nodes),
        edges: fromReactFlowEdges(edges),
        characters,
        variables,
      }

      const { error } = await supabase
        .from('projects')
        .update({
          flowchart_data: flowchartData,
          updated_at: new Date().toISOString(),
        })
        .eq('id', projectId)

      if (error) {
        throw error
      }

      // Clear LocalStorage draft after successful save
      clearDraft(projectId)

      // Update last saved data ref to mark as not dirty
      lastSavedDataRef.current = flowchartData

      // Notify other connected clients to refresh from the database
      realtimeRef.current?.broadcastStateRefresh()

      setToast({ message: 'Project saved successfully', type: 'success' })
    } catch (error) {
      console.error('Failed to save project:', error)
      setToast({
        message: 'Failed to save project.',
        type: 'error',
        action: { label: 'Retry', onClick: () => { setToast(null); handleSaveRef.current() } },
      })
    } finally {
      setIsSaving(false)
    }
  }, [isSaving, nodes, edges, characters, variables, projectId])

  // Keep ref updated with latest handleSave
  handleSaveRef.current = handleSave

  const handleExport = useCallback(() => {
    // Convert React Flow state to FlowchartData
    const flowchartData: FlowchartData = {
      nodes: fromReactFlowNodes(nodes),
      edges: fromReactFlowEdges(edges),
      characters,
      variables,
    }

    // Create pretty-printed JSON
    const jsonContent = JSON.stringify(flowchartData, null, 2)

    // Create blob with JSON content
    const blob = new Blob([jsonContent], { type: 'application/json' })

    // Create download URL
    const url = URL.createObjectURL(blob)

    // Create temporary link element and trigger download
    const link = document.createElement('a')
    link.href = url
    link.download = `${projectName}.vnflow`
    document.body.appendChild(link)
    link.click()

    // Cleanup
    document.body.removeChild(link)
    URL.revokeObjectURL(url)
  }, [nodes, edges, characters, variables, projectName])

  const handleExportRenpy = useCallback(() => {
    // Convert React Flow state to our flowchart types
    const flowchartNodes = fromReactFlowNodes(nodes)
    const flowchartEdges = fromReactFlowEdges(edges)

    // Convert to Ren'Py format
    const renpyExport = convertToRenpyFormat(flowchartNodes, flowchartEdges, projectName)

    // Create pretty-printed JSON
    const jsonContent = JSON.stringify(renpyExport, null, 2)

    // Verify JSON is valid
    try {
      JSON.parse(jsonContent)
    } catch {
      setToast({ message: 'Failed to generate valid JSON', type: 'error' })
      return
    }

    // Create blob with JSON content
    const blob = new Blob([jsonContent], { type: 'application/json' })

    // Create download URL
    const url = URL.createObjectURL(blob)

    // Create temporary link element and trigger download
    const link = document.createElement('a')
    link.href = url
    link.download = `${projectName}-renpy.json`
    document.body.appendChild(link)
    link.click()

    // Cleanup
    document.body.removeChild(link)
    URL.revokeObjectURL(url)

    setToast({ message: 'Exported to Ren\'Py format successfully', type: 'success' })
  }, [nodes, edges, projectName])

  const handleExportAnyway = useCallback(() => {
    setValidationIssues(null)
    setWarningNodeIds(new Set())
    handleExportRenpy()
  }, [handleExportRenpy])

  const handleExportCancel = useCallback(() => {
    setValidationIssues(null)
    setWarningNodeIds(new Set())
  }, [])

  // Check if current flowchart has unsaved changes
  const hasUnsavedChanges = useCallback(() => {
    const currentData: FlowchartData = {
      nodes: fromReactFlowNodes(nodes),
      edges: fromReactFlowEdges(edges),
      characters,
      variables,
    }
    return !flowchartDataEquals(currentData, initialData)
  }, [nodes, edges, characters, variables, initialData])

  // Load imported data into React Flow
  const loadImportedData = useCallback(
    (data: FlowchartData) => {
      setNodes(toReactFlowNodes(data.nodes))
      setEdges(toReactFlowEdges(data.edges))
      setToast({ message: 'Project imported successfully', type: 'success' })
    },
    [setNodes, setEdges]
  )

  // Handle file selection from file picker
  const handleFileSelect = useCallback(
    (event: React.ChangeEvent<HTMLInputElement>) => {
      const file = event.target.files?.[0]
      if (!file) return

      // Reset file input so same file can be selected again
      event.target.value = ''

      const reader = new FileReader()
      reader.onload = (e) => {
        try {
          const content = e.target?.result as string
          const parsedData = JSON.parse(content)

          // Validate the imported data
          if (!isValidFlowchartData(parsedData)) {
            setToast({
              message: 'Invalid file format. File must contain nodes and edges arrays.',
              type: 'error',
            })
            return
          }

          // Check if current project has unsaved changes
          if (hasUnsavedChanges()) {
            // Show confirmation dialog
            setImportConfirmDialog({ pendingData: parsedData })
          } else {
            // Load data directly
            loadImportedData(parsedData)
          }
        } catch {
          setToast({
            message: 'Failed to parse file. Please ensure it is valid JSON.',
            type: 'error',
          })
        }
      }

      reader.onerror = () => {
        setToast({ message: 'Failed to read file.', type: 'error' })
      }

      reader.readAsText(file)
    },
    [hasUnsavedChanges, loadImportedData]
  )

  // Handle import button click - opens file picker
  const handleImport = useCallback(() => {
    fileInputRef.current?.click()
  }, [])

  // Confirm import (discard unsaved changes)
  const handleConfirmImport = useCallback(() => {
    if (importConfirmDialog?.pendingData) {
      loadImportedData(importConfirmDialog.pendingData)
    }
    setImportConfirmDialog(null)
  }, [importConfirmDialog, loadImportedData])

  // Cancel import
  const handleCancelImport = useCallback(() => {
    setImportConfirmDialog(null)
  }, [])

  // Handle edge deletion via keyboard (Delete/Backspace)
  const onEdgesDelete = useCallback((deletedEdges: Edge[]) => {
    // Edges are already removed from state by onEdgesChange
    // This callback can be used for additional logic like logging or dirty state
    console.log('Deleted edges:', deletedEdges.map((e) => e.id))
  }, [])

  // Handle edge click to open condition editor
  const onEdgeClick = useCallback((_event: React.MouseEvent, edge: Edge) => {
    setSelectedEdgeId(edge.id)
  }, [])

  // Handle condition change from ConditionEditor
  const handleConditionChange = useCallback(
    (edgeId: string, condition: Condition | undefined) => {
      setEdges((eds) =>
        eds.map((edge) =>
          edge.id === edgeId
            ? { ...edge, data: condition ? { condition } : undefined }
            : edge
        )
      )
    },
    [setEdges]
  )

  // Context menu handlers
  const closeContextMenu = useCallback(() => {
    setContextMenu(null)
  }, [])

  const onPaneContextMenu = useCallback(
    (event: React.MouseEvent) => {
      event.preventDefault()
      setContextMenu({
        x: event.clientX,
        y: event.clientY,
        type: 'canvas',
      })
    },
    []
  )

  const onNodeContextMenu: NodeMouseHandler = useCallback(
    (event, node) => {
      event.preventDefault()
      setContextMenu({
        x: event.clientX,
        y: event.clientY,
        type: 'node',
        nodeId: node.id,
      })
    },
    []
  )

  const onEdgeContextMenu: EdgeMouseHandler = useCallback(
    (event, edge) => {
      event.preventDefault()
      setContextMenu({
        x: event.clientX,
        y: event.clientY,
        type: 'edge',
        edgeId: edge.id,
      })
    },
    []
  )

  const handleAddNodeAtPosition = useCallback(
    (type: 'dialogue' | 'choice' | 'variable') => {
      if (!contextMenu) return
      const position = screenToFlowPosition({
        x: contextMenu.x,
        y: contextMenu.y,
      })

      let newNode: Node
      if (type === 'dialogue') {
        newNode = {
          id: nanoid(),
          type: 'dialogue',
          position,
          data: { speaker: '', text: '' },
        }
      } else if (type === 'choice') {
        newNode = {
          id: nanoid(),
          type: 'choice',
          position,
          data: {
            prompt: '',
            options: [
              { id: nanoid(), label: '' },
              { id: nanoid(), label: '' },
            ],
          },
        }
      } else {
        newNode = {
          id: nanoid(),
          type: 'variable',
          position,
          data: {
            variableName: '',
            operation: 'set',
            value: 0,
          },
        }
      }

      setNodes((nodes) => [...nodes, newNode])
      setContextMenu(null)
    },
    [contextMenu, screenToFlowPosition, setNodes]
  )

  const handleDeleteNode = useCallback(() => {
    if (!contextMenu?.nodeId) return
    setNodes((nodes) => nodes.filter((n) => n.id !== contextMenu.nodeId))
    setContextMenu(null)
  }, [contextMenu, setNodes])

  const handleDeleteEdge = useCallback(() => {
    if (!contextMenu?.edgeId) return
    setEdges((edges) => edges.filter((e) => e.id !== contextMenu.edgeId))
    setContextMenu(null)
  }, [contextMenu, setEdges])

  const openConditionEditor = useCallback(
    (edgeId: string) => {
      const edge = edges.find((e) => e.id === edgeId)
      if (!edge) return
      setConditionEditor({
        edgeId,
        condition: edge.data?.condition,
      })
    },
    [edges]
  )

  const handleAddCondition = useCallback(() => {
    if (!contextMenu?.edgeId) return
    openConditionEditor(contextMenu.edgeId)
    setContextMenu(null)
  }, [contextMenu, openConditionEditor])

  const onEdgeDoubleClick = useCallback(
    (_event: React.MouseEvent, edge: Edge) => {
      openConditionEditor(edge.id)
    },
    [openConditionEditor]
  )

  // Apply warning styles to nodes with undefined references
  const styledNodes = useMemo(
    () =>
      warningNodeIds.size === 0
        ? nodes
        : nodes.map((node) =>
            warningNodeIds.has(node.id)
              ? { ...node, className: 'export-warning-node' }
              : node
          ),
    [nodes, warningNodeIds]
  )

  // Get the selected edge's condition data
  const selectedEdge = useMemo(
    () => (selectedEdgeId ? edges.find((e) => e.id === selectedEdgeId) : null),
    [selectedEdgeId, edges]
  )

  return (
    <EditorProvider value={editorContextValue}>
      <div className="flex h-screen w-full flex-col">
        <Toolbar
          onAddDialogue={handleAddDialogue}
          onAddChoice={handleAddChoice}
          onAddVariable={handleAddVariable}
          onSave={handleSave}
          isSaving={isSaving}
          onExport={handleExport}
          onExportRenpy={handleExportRenpy}
          onImport={handleImport}
          onProjectSettings={() => setShowSettings(true)}
          onShare={() => setShowShare(true)}
          onHistory={() => setShowHistory((v) => !v)}
          connectionState={connectionState}
          presenceUsers={presenceUsers}
        />
        <div className="relative flex-1" onMouseMove={handleMouseMove}>
          <ReactFlow
            nodes={styledNodes}
            edges={edges}
            nodeTypes={nodeTypes}
            edgeTypes={edgeTypes}
            onNodesChange={onNodesChange}
            onEdgesChange={onEdgesChange}
            onEdgesDelete={onEdgesDelete}
            onEdgeClick={onEdgeClick}
            onConnect={onConnect}
            onPaneContextMenu={onPaneContextMenu}
            onNodeContextMenu={onNodeContextMenu}
            onEdgeContextMenu={onEdgeContextMenu}
            onEdgeDoubleClick={onEdgeDoubleClick}
            deleteKeyCode={['Delete', 'Backspace']}
            fitView
          >
            <Background variant={BackgroundVariant.Dots} gap={16} size={1} />
            <Controls position="bottom-right" />
          </ReactFlow>
          <RemoteCursors cursors={remoteCursors} />
          {showHistory && (
            <ActivityHistorySidebar
              projectId={projectId}
              onClose={() => setShowHistory(false)}
              onSelectEntity={handleHistorySelectEntity}
              onRevert={handleRevertEntry}
            />
          )}
        </div>
        {contextMenu && (
          <ContextMenu
            x={contextMenu.x}
            y={contextMenu.y}
            type={contextMenu.type}
            onClose={closeContextMenu}
            onAddDialogue={() => handleAddNodeAtPosition('dialogue')}
            onAddChoice={() => handleAddNodeAtPosition('choice')}
            onAddVariable={() => handleAddNodeAtPosition('variable')}
            onDelete={
              contextMenu.type === 'node' ? handleDeleteNode : handleDeleteEdge
            }
            onAddCondition={handleAddCondition}
          />
        )}
        {showSettings && (
          <ProjectSettingsModal
            projectId={projectId}
            characters={characters}
            variables={variables}
            onCharactersChange={setCharacters}
            onVariablesChange={setVariables}
            onClose={() => setShowSettings(false)}
            getCharacterUsageCount={getCharacterUsageCount}
            getVariableUsageCount={getVariableUsageCount}
          />
        )}
        {showShare && (
          <ShareModal
            projectId={projectId}
            isOwner={isOwner}
            onClose={() => setShowShare(false)}
          />
        )}
        {selectedEdge && (
          <ConditionEditor
            edgeId={selectedEdge.id}
            condition={selectedEdge.data?.condition}
            onChange={handleConditionChange}
            onClose={() => setSelectedEdgeId(null)}
          />
        )}
        {validationIssues && (
          <ExportValidationModal
            issues={validationIssues}
            onExportAnyway={handleExportAnyway}
            onCancel={handleExportCancel}
          />
        )}
        {toastMessage && (
          <Toast
            message={toastMessage}
            type="success"
            onClose={() => setToastMessage(null)}
          />
        )}
        {collaborationNotifications.length > 0 && (
          <div className="fixed bottom-4 left-4 z-50 flex flex-col gap-2">
            {collaborationNotifications.map((notification) => (
              <CollaborationToast
                key={notification.id}
                notification={notification}
                onDismiss={handleDismissNotification}
              />
            ))}
          </div>
        )}
      </div>
    </EditorProvider>
  )
}

// Outer wrapper component with ReactFlowProvider
export default function FlowchartEditor(props: FlowchartEditorProps) {
  return (
    <ReactFlowProvider>
      <FlowchartEditorInner {...props} />
    </ReactFlowProvider>
  )
}
