import { useState, useCallback, useEffect, useRef, useReducer } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import {
  ReactFlowProvider,
  useNodesState,
  useEdgesState,
  addEdge,
  reconnectEdge,
  useReactFlow,
  getNodesBounds,
  getViewportForBounds,
  type Node,
  type Edge,
  type Connection,
} from '@xyflow/react'
import '@xyflow/react/dist/style.css'

import { NetworkCanvas } from '@/components/network/NetworkCanvas'
import { ContextMenu, getNodeMenuActions, getCanvasMenuActions } from '@/components/network/ContextMenu'
import { useCanvasShortcuts } from '@/components/network/hooks/useCanvasShortcuts'
import { useDiagramCommands } from '@/components/network/hooks/useDiagramCommands'
import { DiagramHeader, type InteractionMode } from '@/components/network/DiagramHeader'
import { DeviceToolbar } from '@/components/network/panels/DeviceToolbar'
import { PropertiesPanel } from '@/components/network/panels/PropertiesPanel'
import { AIAssistPanel } from '@/components/network/panels/AIAssistPanel'
import { CanvasEmptyPrompt } from '@/components/network/CanvasEmptyPrompt'
import { KeyboardShortcutsOverlay } from '@/components/network/KeyboardShortcutsOverlay'
import { networkDiagramsApi, deviceTypesApi } from '@/api'
import { toast } from '@/lib/toast'
import { exportToDrawio } from '@/lib/drawio-export'
import { parseDrawioXml } from '@/lib/drawio-import'
import type { DeviceTypeResponse, DeviceProperties, AIGenerateResponse, DiagramEdge, DiagramNode } from '@/types'
import type { DeviceNodeData } from '@/components/network/nodes/DeviceNode'

function normalizeZOrder(nodes: Node[]): Node[] {
  const sorted = [...nodes].sort((a, b) => ((a.zIndex ?? 0) - (b.zIndex ?? 0)))
  return sorted.map((n, i) => ({ ...n, zIndex: i + 1 }))
}

type ContextMenuState = {
  type: 'node' | 'canvas'
  position: { x: number; y: number }
  nodeId?: string
} | null

function DiagramEditorInner() {
  const { id } = useParams<{ id: string }>()
  const navigate = useNavigate()
  const { getNodes, fitView, screenToFlowPosition } = useReactFlow()

  const [diagramId, setDiagramId] = useState<string | null>(id || null)
  const [name, setName] = useState('Untitled Diagram')
  const [clientName, setClientName] = useState<string | null>(null)
  const [assetName, setAssetName] = useState<string | null>(null)
  const [description, setDescription] = useState<string | null>(null)

  const [nodes, setNodes, onNodesChange] = useNodesState<Node>([])
  const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([])

  const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null)
  const [selectedEdgeId, setSelectedEdgeId] = useState<string | null>(null)

  const selectedNode = selectedNodeId ? nodes.find(n => n.id === selectedNodeId) ?? null : null
  const selectedEdge = selectedEdgeId ? edges.find(e => e.id === selectedEdgeId) ?? null : null

  const [isDirty, setIsDirty] = useState(false)
  const [isSaving, setIsSaving] = useState(false)
  const [lastSavedAt, setLastSavedAt] = useState<Date | null>(null)
  const isDirtyRef = useRef(false)
  const diagramIdRef = useRef<string | null>(id || null)

  const [deviceTypes, setDeviceTypes] = useState<DeviceTypeResponse[]>([])
  const [loading, setLoading] = useState(!!id)
  const [isDragOver, setIsDragOver] = useState(false)

  const [interactionMode, setInteractionMode] = useState<InteractionMode>('select')
  const [showShortcuts, setShowShortcuts] = useState(false)

  const canvasRef = useRef<HTMLDivElement | null>(null)
  const drawioImportRef = useRef<HTMLInputElement>(null)
  const [contextMenu, setContextMenu] = useState<ContextMenuState>(null)
  const [pendingDeleteNodeId, setPendingDeleteNodeId] = useState<string | null>(null)

  useEffect(() => { isDirtyRef.current = isDirty }, [isDirty])
  useEffect(() => { diagramIdRef.current = diagramId }, [diagramId])

  // History
  const historyStack = useRef<{ nodes: Node[]; edges: Edge[] }[]>([])
  const historyIndex = useRef<number>(-1)
  const MAX_HISTORY = 50
  const [, forceHistoryUpdate] = useReducer((x: number) => x + 1, 0)

  const pushHistory = useCallback((currentNodes: Node[], currentEdges: Edge[]) => {
    historyStack.current = historyStack.current.slice(0, historyIndex.current + 1)
    historyStack.current.push({
      nodes: JSON.parse(JSON.stringify(currentNodes)),
      edges: JSON.parse(JSON.stringify(currentEdges)),
    })
    if (historyStack.current.length > MAX_HISTORY) {
      historyStack.current.shift()
    } else {
      historyIndex.current += 1
    }
    forceHistoryUpdate()
  }, [forceHistoryUpdate])

  const undo = useCallback(() => {
    if (historyIndex.current <= 0) return
    historyIndex.current -= 1
    const snapshot = historyStack.current[historyIndex.current]
    setNodes(snapshot.nodes)
    setEdges(snapshot.edges)
    setIsDirty(true)
    forceHistoryUpdate()
  }, [setNodes, setEdges, forceHistoryUpdate])

  const redo = useCallback(() => {
    if (historyIndex.current >= historyStack.current.length - 1) return
    historyIndex.current += 1
    const snapshot = historyStack.current[historyIndex.current]
    setNodes(snapshot.nodes)
    setEdges(snapshot.edges)
    setIsDirty(true)
    forceHistoryUpdate()
  }, [setNodes, setEdges, forceHistoryUpdate])

  const canUndo = historyIndex.current > 0
  const canRedo = historyIndex.current < historyStack.current.length - 1

  const diagramCommands = useDiagramCommands({
    nodes,
    edges,
    pushHistory,
    setNodes,
  })

  const onNudge = useCallback((dx: number, dy: number) => {
    const selected = nodes.filter(n => n.selected)
    if (selected.length === 0) return
    pushHistory(nodes, edges)
    setNodes(prev => prev.map(n =>
      n.selected
        ? { ...n, position: { x: n.position.x + dx, y: n.position.y + dy } }
        : n
    ))
  }, [nodes, edges, pushHistory, setNodes])

  const {
    copyNodes,
    pasteNodes,
    duplicateNodes,
    selectAll,
    deleteSelected,
    hasClipboard,
  } = useCanvasShortcuts({
    nodes,
    edges,
    setNodes,
    setEdges,
    setIsDirty: (v: boolean) => setIsDirty(v),
    canvasRef,
    onUndo: undo,
    onRedo: redo,
    onNudge,
    onSetMode: setInteractionMode,
    onToggleShortcuts: () => setShowShortcuts(v => !v),
  })

  const handleNodesChange: typeof onNodesChange = useCallback((changes) => {
    onNodesChange(changes)
    const hasRealChange = changes.some(c => c.type !== 'select')
    if (hasRealChange) setIsDirty(true)
  }, [onNodesChange])

  const handleEdgesChange: typeof onEdgesChange = useCallback((changes) => {
    onEdgesChange(changes)
    const hasRealChange = changes.some(c => c.type !== 'select')
    if (hasRealChange) setIsDirty(true)
  }, [onEdgesChange])

  const loadDeviceTypes = useCallback(async () => {
    try {
      const types = await deviceTypesApi.list()
      setDeviceTypes(types)
    } catch { /* ignore */ }
  }, [])

  useEffect(() => { loadDeviceTypes() }, [loadDeviceTypes])

  useEffect(() => {
    if (!id) return
    let cancelled = false
    ;(async () => {
      try {
        const diagram = await networkDiagramsApi.get(id)
        if (cancelled) return
        setName(diagram.name)
        setClientName(diagram.client_name)
        setAssetName(diagram.asset_name)
        setDescription(diagram.description)
        setNodes(
          diagram.nodes.map(n => {
            if (n.nodeType === 'group') {
              return {
                id: n.id,
                type: 'group',
                position: n.position,
                style: n.style || { width: 300, height: 200 },
                data: {
                  label: n.label,
                  groupType: n.type,
                },
              }
            }
            return {
              id: n.id,
              type: 'device',
              position: n.position,
              style: n.style || { width: 120, height: 120 },
              ...(n.parentId ? { parentId: n.parentId, extent: 'parent' as const } : {}),
              data: {
                label: n.label,
                deviceType: n.type,
                properties: n.properties,
              } satisfies DeviceNodeData,
            }
          })
        )
        setEdges(
          diagram.edges.map(e => ({
            id: e.id,
            source: e.source,
            target: e.target,
            type: 'connection',
            label: e.label || undefined,
            data: {
              connectionType: e.connectionType,
              speed: e.speed,
              notes: e.notes,
              routing: e.routing ?? null,
            },
          }))
        )
        setLastSavedAt(new Date(diagram.updated_at))
        // Initialize history after load
        const loadedNodes = diagram.nodes.map(n => {
          if (n.nodeType === 'group') {
            return {
              id: n.id,
              type: 'group' as const,
              position: n.position,
              style: n.style || { width: 300, height: 200 },
              data: { label: n.label, groupType: n.type },
            }
          }
          return {
            id: n.id,
            type: 'device' as const,
            position: n.position,
            style: n.style || { width: 120, height: 120 },
            ...(n.parentId ? { parentId: n.parentId, extent: 'parent' as const } : {}),
            data: { label: n.label, deviceType: n.type, properties: n.properties } satisfies DeviceNodeData,
          }
        })
        const loadedEdges = diagram.edges.map(e => ({
          id: e.id,
          source: e.source,
          target: e.target,
          type: 'connection' as const,
          label: e.label || undefined,
          data: { connectionType: e.connectionType, speed: e.speed, notes: e.notes, routing: e.routing ?? null },
        }))
        historyStack.current = []
        historyIndex.current = -1
        pushHistory(loadedNodes, loadedEdges)
      } catch {
        toast.error('Failed to load diagram')
        navigate('/network-diagrams')
      } finally {
        if (!cancelled) setLoading(false)
      }
    })()
    return () => { cancelled = true }
  }, [id, navigate, setNodes, setEdges, pushHistory])

  const serializeNodes = useCallback((): DiagramNode[] => {
    return getNodes().map(n => {
      if (n.type === 'group') {
        const data = n.data as Record<string, unknown>
        const width = typeof n.style?.width === 'number' ? n.style.width : (n.measured?.width ?? 300)
        const height = typeof n.style?.height === 'number' ? n.style.height : (n.measured?.height ?? 200)
        return {
          id: n.id,
          type: (data.groupType as string) || 'subnet',
          label: (data.label as string) || 'Group',
          position: n.position,
          properties: {} as DeviceProperties,
          nodeType: 'group',
          style: { width, height },
        }
      }
      const data = n.data as unknown as DeviceNodeData
      const dw = typeof n.style?.width === 'number' ? n.style.width : (n.measured?.width ?? 120)
      const dh = typeof n.style?.height === 'number' ? n.style.height : (n.measured?.height ?? 120)
      return {
        id: n.id,
        type: data.deviceType,
        label: data.label,
        position: n.position,
        properties: data.properties,
        style: { width: dw, height: dh },
        ...(n.parentId ? { parentId: n.parentId } : {}),
      }
    })
  }, [getNodes])

  const serializeEdges = useCallback((): DiagramEdge[] => {
    return edges.map(e => {
      const d = (e.data as Record<string, unknown>) || {}
      return {
        id: e.id,
        source: e.source,
        target: e.target,
        label: (e.label as string) || null,
        connectionType: d.connectionType as string || 'ethernet',
        speed: d.speed as string || null,
        notes: d.notes as string || null,
        routing: (d.routing as DiagramEdge['routing']) || null,
      }
    })
  }, [edges])

  const handleSave = useCallback(async () => {
    setIsSaving(true)
    try {
      const payload = {
        name,
        client_name: clientName,
        asset_name: assetName,
        description,
        nodes: serializeNodes(),
        edges: serializeEdges(),
      }
      let savedId: string | null = diagramIdRef.current
      if (diagramIdRef.current) {
        await networkDiagramsApi.update(diagramIdRef.current, payload)
      } else {
        const created = await networkDiagramsApi.create(payload)
        savedId = created.id
        setDiagramId(created.id)
        navigate(`/network-diagrams/${created.id}`, { replace: true })
      }
      setIsDirty(false)
      setLastSavedAt(new Date())

      // Generate thumbnail in the background — don't block save UX on failure
      if (savedId && nodes.length > 0) {
        try {
          const { toPng } = await import('html-to-image')
          const THUMB_W = 480
          const THUMB_H = 300
          const bounds = getNodesBounds(nodes)
          const viewport = getViewportForBounds(bounds, THUMB_W, THUMB_H, 0.5, 2, 0.1)
          const flowEl = document.querySelector('.react-flow__viewport') as HTMLElement | null
          if (flowEl) {
            const dataUrl = await toPng(flowEl, {
              backgroundColor: '#16181f',
              width: THUMB_W,
              height: THUMB_H,
              style: {
                width: `${THUMB_W}px`,
                height: `${THUMB_H}px`,
                transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`,
                transformOrigin: 'top left',
              },
            })
            await networkDiagramsApi.uploadThumbnail(savedId, dataUrl)
          }
        } catch {
          // Thumbnail failure is silent — doesn't affect save success
        }
      }
    } catch {
      toast.error('Failed to save diagram')
    } finally {
      setIsSaving(false)
    }
  }, [name, clientName, assetName, description, serializeNodes, serializeEdges, navigate, nodes])

  useEffect(() => {
    const interval = setInterval(() => {
      if (isDirtyRef.current && diagramIdRef.current) {
        handleSave()
      }
    }, 30_000)
    return () => clearInterval(interval)
  }, [handleSave])

  const onConnect = useCallback((connection: Connection) => {
    pushHistory(nodes, edges)
    setEdges(eds => addEdge({
      ...connection,
      type: 'connection',
      data: { connectionType: 'ethernet' },
    }, eds))
    setIsDirty(true)
  }, [nodes, edges, pushHistory, setEdges])

  const onReconnect = useCallback((oldEdge: Edge, newConnection: Connection) => {
    pushHistory(nodes, edges)
    setEdges(eds => reconnectEdge(oldEdge, newConnection, eds))
    setSelectedEdgeId(oldEdge.id)
    setIsDirty(true)
  }, [nodes, edges, pushHistory, setEdges])

  const onDragOver = useCallback((event: React.DragEvent) => {
    event.preventDefault()
    event.dataTransfer.dropEffect = 'move'
    setIsDragOver(true)
  }, [])

  const onDragLeave = useCallback((event: React.DragEvent) => {
    const relatedTarget = event.relatedTarget as HTMLElement | null
    if (relatedTarget && (event.currentTarget as HTMLElement).contains(relatedTarget)) return
    setIsDragOver(false)
  }, [])

  const handleNodeContextMenu = useCallback((event: React.MouseEvent, node: Node) => {
    event.preventDefault()
    const currentNodes = getNodes()
    const isInSelection = currentNodes.find(n => n.id === node.id)?.selected
    if (!isInSelection) {
      setNodes(nds => nds.map(n => ({ ...n, selected: n.id === node.id })))
      setSelectedNodeId(node.id)
      setSelectedEdgeId(null)
    }
    setContextMenu({
      type: 'node',
      position: { x: event.clientX, y: event.clientY },
      nodeId: node.id,
    })
  }, [getNodes, setNodes, setSelectedNodeId, setSelectedEdgeId])

  const handlePaneContextMenu = useCallback((event: MouseEvent | React.MouseEvent) => {
    event.preventDefault()
    // Group nodes pass pointer events through to children, so right-clicking a group
    // may fire onPaneContextMenu instead of onNodeContextMenu. If nodes are selected,
    // show the node context menu so group/align/ungroup options are accessible.
    const selected = getNodes().filter(n => n.selected)
    if (selected.length > 0) {
      setContextMenu({
        type: 'node',
        position: { x: event.clientX, y: event.clientY },
      })
    } else {
      setContextMenu({
        type: 'canvas',
        position: { x: event.clientX, y: event.clientY },
      })
    }
  }, [getNodes])

  const closeContextMenu = useCallback(() => {
    setContextMenu(null)
  }, [])

  const onDrop = useCallback((event: React.DragEvent) => {
    event.preventDefault()
    setIsDragOver(false)

    const position = screenToFlowPosition({ x: event.clientX, y: event.clientY })

    // Handle device drops
    const deviceRaw = event.dataTransfer.getData('application/reactflow-device')
    if (deviceRaw) {
      const { slug, label, category } = JSON.parse(deviceRaw) as { slug: string; label: string; category: string }
      const newNode: Node = {
        id: `device-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
        type: 'device',
        position,
        style: { width: 120, height: 120 },
        data: {
          label,
          deviceType: slug,
          category,
          properties: {
            hostname: null,
            ip: null,
            subnet: null,
            vendor: null,
            model: null,
            role: null,
            vlan: null,
            notes: null,
            status: 'unknown',
          } satisfies DeviceProperties,
        } satisfies DeviceNodeData,
      }
      pushHistory(nodes, edges)
      setNodes(nds => [...nds, newNode])
      setIsDirty(true)
      return
    }

    // Handle group drops
    const groupRaw = event.dataTransfer.getData('application/reactflow-group')
    if (groupRaw) {
      const { slug, label } = JSON.parse(groupRaw) as { slug: string; label: string }
      const newNode: Node = {
        id: `group-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
        type: 'group',
        position,
        style: { width: 300, height: 200 },
        data: {
          label,
          groupType: slug,
        },
      }
      pushHistory(nodes, edges)
      setNodes(nds => [...nds, newNode])
      setIsDirty(true)
    }
  }, [nodes, edges, pushHistory, setNodes, screenToFlowPosition])

  const handleNodeUpdate = useCallback((nodeId: string, updates: Partial<DeviceNodeData>) => {
    pushHistory(nodes, edges)
    setNodes(nds => nds.map(n => {
      if (n.id !== nodeId) return n
      return { ...n, data: { ...n.data, ...updates } }
    }))
    setIsDirty(true)
  }, [nodes, edges, pushHistory, setNodes])

  const handleEdgeUpdate = useCallback((edgeId: string, updates: Partial<DiagramEdge>) => {
    pushHistory(nodes, edges)
    setEdges(eds => eds.map(e => {
      if (e.id !== edgeId) return e
      return {
        ...e,
        label: updates.label !== undefined ? (updates.label || undefined) : e.label,
        data: {
          ...(e.data || {}),
          ...(updates.connectionType !== undefined ? { connectionType: updates.connectionType } : {}),
          ...(updates.speed !== undefined ? { speed: updates.speed } : {}),
          ...(updates.notes !== undefined ? { notes: updates.notes } : {}),
          ...(updates.routing !== undefined ? { routing: updates.routing } : {}),
        },
      }
    }))
    setIsDirty(true)
  }, [nodes, edges, pushHistory, setEdges])

  const handleEdgeTypeChange = useCallback((edgeId: string, edgeType: string) => {
    pushHistory(nodes, edges)
    setEdges(eds => eds.map(e => {
      if (e.id !== edgeId) return e
      return { ...e, type: edgeType }
    }))
    setIsDirty(true)
  }, [nodes, edges, pushHistory, setEdges])

  const handleDeleteNode = useCallback((nodeId: string) => {
    pushHistory(nodes, edges)
    setNodes(nds => nds.filter(n => n.id !== nodeId))
    setEdges(eds => eds.filter(e => e.source !== nodeId && e.target !== nodeId))
    setSelectedNodeId(null)
    setIsDirty(true)
  }, [nodes, edges, pushHistory, setNodes, setEdges])

  const handleDeleteEdge = useCallback((edgeId: string) => {
    pushHistory(nodes, edges)
    setEdges(eds => eds.filter(e => e.id !== edgeId))
    setSelectedEdgeId(null)
    setIsDirty(true)
  }, [nodes, edges, pushHistory, setEdges])

  const handleBringToFront = useCallback((nodeId: string) => {
    pushHistory(nodes, edges)
    setNodes(prev => {
      const maxZ = Math.max(0, ...prev.map(n => n.zIndex ?? 0))
      return normalizeZOrder(
        prev.map(n => n.id === nodeId ? { ...n, zIndex: maxZ + 1 } : n)
      )
    })
    setIsDirty(true)
  }, [nodes, edges, pushHistory, setNodes])

  const handleSendToBack = useCallback((nodeId: string) => {
    pushHistory(nodes, edges)
    setNodes(prev => normalizeZOrder(
      prev.map(n => n.id === nodeId ? { ...n, zIndex: 0 } : n)
    ))
    setIsDirty(true)
  }, [nodes, edges, pushHistory, setNodes])

  const handleAIGenerate = useCallback((result: AIGenerateResponse, mode: 'replace' | 'merge') => {
    const newNodes: Node[] = result.nodes.map(n => ({
      id: n.id,
      type: 'device',
      position: n.position,
      data: {
        label: n.label,
        deviceType: n.type,
        properties: n.properties,
      } satisfies DeviceNodeData,
    }))

    const newEdges: Edge[] = result.edges.map(e => ({
      id: e.id,
      source: e.source,
      target: e.target,
      type: 'connection',
      label: e.label || undefined,
      data: { connectionType: e.connectionType, speed: e.speed, notes: e.notes },
    }))

    pushHistory(nodes, edges)
    if (mode === 'replace') {
      setNodes(newNodes)
      setEdges(newEdges)
    } else {
      setNodes(nds => [...nds, ...newNodes])
      setEdges(eds => [...eds, ...newEdges])
    }

    if (result.suggestedName && !diagramId) {
      setName(result.suggestedName)
      toast.success(`Generated: ${result.suggestedName}`)
    } else {
      toast.success('Diagram generated')
    }

    if (result.notes) {
      toast.info(result.notes)
    }

    setIsDirty(true)
    setTimeout(() => fitView({ padding: 0.2 }), 100)
  }, [nodes, edges, pushHistory, setNodes, setEdges, diagramId, fitView])

  const getExistingBounds = useCallback(() => {
    const currentNodes = getNodes()
    if (currentNodes.length === 0) return null
    let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity
    for (const n of currentNodes) {
      minX = Math.min(minX, n.position.x)
      maxX = Math.max(maxX, n.position.x + 120)
      minY = Math.min(minY, n.position.y)
      maxY = Math.max(maxY, n.position.y + 80)
    }
    return { minX, maxX, minY, maxY }
  }, [getNodes])

  const handleExportPng = useCallback(async () => {
    if (nodes.length === 0) {
      toast.warning('Add some devices to the diagram before exporting')
      return
    }
    try {
      const { toPng } = await import('html-to-image')
      const IMAGE_WIDTH = 1920
      const IMAGE_HEIGHT = 1080
      const bounds = getNodesBounds(nodes)
      const viewport = getViewportForBounds(bounds, IMAGE_WIDTH, IMAGE_HEIGHT, 0.5, 2, 0.15)
      const flowEl = document.querySelector('.react-flow__viewport') as HTMLElement | null
      if (!flowEl) {
        toast.error('Could not find canvas to export')
        return
      }
      const dataUrl = await toPng(flowEl, {
        backgroundColor: '#16181f',
        width: IMAGE_WIDTH,
        height: IMAGE_HEIGHT,
        style: {
          width: `${IMAGE_WIDTH}px`,
          height: `${IMAGE_HEIGHT}px`,
          transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`,
          transformOrigin: 'top left',
        },
      })
      const a = document.createElement('a')
      a.download = `${name.replace(/[^a-zA-Z0-9-_ ]/g, '') || 'diagram'}.png`
      a.href = dataUrl
      a.click()
    } catch {
      toast.error('PNG export failed — try Print > Save as PDF instead')
    }
  }, [nodes, name])

  const handleExportSvg = useCallback(async () => {
    if (nodes.length === 0) {
      toast.warning('Add some devices to the diagram before exporting')
      return
    }
    try {
      const { toSvg } = await import('html-to-image')
      const IMAGE_WIDTH = 1920
      const IMAGE_HEIGHT = 1080
      const bounds = getNodesBounds(nodes)
      const viewport = getViewportForBounds(bounds, IMAGE_WIDTH, IMAGE_HEIGHT, 0.5, 2, 0.15)
      const flowEl = document.querySelector('.react-flow__viewport') as HTMLElement | null
      if (!flowEl) {
        toast.error('Could not find canvas to export')
        return
      }
      const dataUrl = await toSvg(flowEl, {
        backgroundColor: '#16181f',
        width: IMAGE_WIDTH,
        height: IMAGE_HEIGHT,
        style: {
          width: `${IMAGE_WIDTH}px`,
          height: `${IMAGE_HEIGHT}px`,
          transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`,
          transformOrigin: 'top left',
        },
      })
      const a = document.createElement('a')
      a.download = `${name.replace(/[^a-zA-Z0-9-_ ]/g, '') || 'diagram'}.svg`
      a.href = dataUrl
      a.click()
    } catch {
      toast.error('SVG export failed')
    }
  }, [nodes, name])

  const handleExportPdf = useCallback(() => {
    window.print()
  }, [])

  const handleExportJson = useCallback(async () => {
    if (!diagramId) return
    try {
      const data = await networkDiagramsApi.exportJson(diagramId)
      const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
      const url = URL.createObjectURL(blob)
      const a = document.createElement('a')
      a.href = url
      a.download = `${name.replace(/[^a-zA-Z0-9-_ ]/g, '')}.json`
      a.click()
      URL.revokeObjectURL(url)
    } catch {
      toast.error('Failed to export diagram')
    }
  }, [diagramId, name])

  const handleExportDrawio = useCallback(() => {
    if (nodes.length === 0) {
      toast.warning('Add some devices to the diagram before exporting')
      return
    }
    const xml = exportToDrawio(getNodes(), edges)
    const blob = new Blob([xml], { type: 'application/xml' })
    const url = URL.createObjectURL(blob)
    const a = document.createElement('a')
    a.href = url
    a.download = `${name.replace(/[^a-zA-Z0-9-_ ]/g, '') || 'diagram'}.drawio`
    a.click()
    URL.revokeObjectURL(url)
  }, [nodes, edges, getNodes, name])

  const handleImportDrawio = useCallback(() => {
    drawioImportRef.current?.click()
  }, [])

  const handleDrawioFileChange = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0]
    if (!file) return
    e.target.value = ''
    try {
      const text = await file.text()
      const { nodes: importedNodes, edges: importedEdges, warnings } = parseDrawioXml(text)
      const importPayload = {
        schemaVersion: 1 as const,
        name: file.name.replace(/\.drawio$/i, '') || 'Imported Diagram',
        client_name: null,
        description: null,
        nodes: importedNodes,
        edges: importedEdges,
      }
      const result = await networkDiagramsApi.importJson(importPayload)
      const allWarnings = [...warnings, ...result.warnings]
      if (allWarnings.length > 0) {
        toast.warning(`Imported with ${allWarnings.length} warning(s): ${allWarnings[0]}`)
      } else {
        toast.success('draw.io file imported successfully')
      }
      navigate(`/network-diagrams/${result.diagram.id}`)
    } catch (err) {
      const msg = err instanceof Error ? err.message : 'Unknown error'
      toast.error(`Import failed: ${msg}`)
    }
  }, [navigate])

  if (loading) {
    return (
      <div className="flex h-full items-center justify-center">
        <div className="h-6 w-6 animate-spin rounded-full border-2 border-accent border-t-transparent" />
      </div>
    )
  }

  return (
    <div className="flex h-full flex-col">
      <DiagramHeader
        name={name}
        clientName={clientName}
        isDirty={isDirty}
        isSaving={isSaving}
        lastSavedAt={lastSavedAt}
        diagramId={diagramId}
        onNameChange={(n: string) => { setName(n); setIsDirty(true) }}
        onSave={handleSave}
        onExportPng={handleExportPng}
        onExportSvg={handleExportSvg}
        onExportPdf={handleExportPdf}
        onExportJson={handleExportJson}
        onExportDrawio={handleExportDrawio}
        onImportDrawio={handleImportDrawio}
        onUndo={undo}
        onRedo={redo}
        canUndo={canUndo}
        canRedo={canRedo}
        interactionMode={interactionMode}
        onModeChange={setInteractionMode}
      />
      <div className="flex flex-1 min-h-0">
        <DeviceToolbar deviceTypes={deviceTypes} onDeviceTypesChange={loadDeviceTypes} />
        <div className="flex flex-1 flex-col min-h-0">
          <div className="relative flex-1 min-h-0" ref={canvasRef}>
            <NetworkCanvas
              nodes={nodes}
              edges={edges}
              onNodesChange={handleNodesChange}
              onEdgesChange={handleEdgesChange}
              onConnect={onConnect}
              onReconnect={onReconnect}
              onNodeSelect={setSelectedNodeId}
              onEdgeSelect={setSelectedEdgeId}
              onDrop={onDrop}
              onDragOver={onDragOver}
              onDragLeave={onDragLeave}
              isDragOver={isDragOver}
              onNodeContextMenu={handleNodeContextMenu}
              onPaneContextMenu={handlePaneContextMenu}
              onPaneClick={closeContextMenu}
              interactionMode={interactionMode}
            />
            {interactionMode === 'connect' && (
              <div className="pointer-events-none absolute left-1/2 top-4 z-10 -translate-x-1/2 rounded-full border border-accent/30 bg-card/95 px-3 py-1.5 text-[11px] text-muted-foreground">
                Connect mode: drag between device handles. Middle-click and drag to pan.
              </div>
            )}
            {nodes.length === 0 && !loading && (
              <CanvasEmptyPrompt onGenerate={handleAIGenerate} />
            )}
            {/* Keyboard shortcut hint button — above the MiniMap */}
            <button
              onClick={() => setShowShortcuts(true)}
              title="Keyboard shortcuts (?)"
              className="absolute bottom-[175px] right-3 z-10 flex h-6 w-6 items-center justify-center rounded-full border border-default bg-card text-[11px] font-semibold text-muted-foreground hover:border-accent hover:text-accent transition-colors"
            >
              ?
            </button>
          </div>
          {nodes.length > 0 && (
            <AIAssistPanel
              onGenerate={handleAIGenerate}
              getExistingBounds={getExistingBounds}
              hasNodes={nodes.length > 0}
            />
          )}
        </div>
        <PropertiesPanel
          selectedNode={selectedNode}
          selectedEdge={selectedEdge}
          onNodeUpdate={handleNodeUpdate}
          onEdgeUpdate={handleEdgeUpdate}
          onEdgeTypeChange={handleEdgeTypeChange}
          onBringToFront={handleBringToFront}
          onSendToBack={handleSendToBack}
          onDeleteNode={handleDeleteNode}
          onDeleteEdge={handleDeleteEdge}
          selectedNodeCount={nodes.filter(n => n.selected).length}
          onAlignLeft={diagramCommands.alignLeft}
          onAlignRight={diagramCommands.alignRight}
          onAlignCenterH={diagramCommands.alignCenterH}
          onAlignTop={diagramCommands.alignTop}
          onAlignBottom={diagramCommands.alignBottom}
          onAlignCenterV={diagramCommands.alignCenterV}
          onDistributeH={diagramCommands.distributeHorizontally}
          onDistributeV={diagramCommands.distributeVertically}
          canAlign={diagramCommands.canAlign}
          canDistribute={diagramCommands.canDistribute}
          canGroup={diagramCommands.canGroup}
          canUngroup={diagramCommands.canUngroup}
          onGroupSelection={diagramCommands.groupSelection}
          onUngroupSelection={diagramCommands.ungroupSelection}
        />
      </div>
      {contextMenu && (
        <ContextMenu
          position={contextMenu.position}
          actions={
            contextMenu.type === 'node'
              ? getNodeMenuActions({
                  onCopy: copyNodes,
                  onDuplicate: duplicateNodes,
                  onBringToFront: () => { if (contextMenu.nodeId) handleBringToFront(contextMenu.nodeId) },
                  onSendToBack: () => { if (contextMenu.nodeId) handleSendToBack(contextMenu.nodeId) },
                  onDelete: () => {
                    const nodeId = contextMenu.nodeId
                    setContextMenu(null)
                    if (nodeId) setPendingDeleteNodeId(nodeId)
                    else deleteSelected()
                  },
                })
              : getCanvasMenuActions({
                  onPaste: pasteNodes,
                  onSelectAll: selectAll,
                  onFitView: () => fitView({ padding: 0.2 }),
                  hasClipboard: hasClipboard(),
                })
          }
          onClose={closeContextMenu}
          onAlignLeft={diagramCommands.alignLeft}
          onAlignRight={diagramCommands.alignRight}
          onAlignCenterH={diagramCommands.alignCenterH}
          onAlignTop={diagramCommands.alignTop}
          onAlignBottom={diagramCommands.alignBottom}
          onAlignCenterV={diagramCommands.alignCenterV}
          onDistributeH={diagramCommands.distributeHorizontally}
          onDistributeV={diagramCommands.distributeVertically}
          canAlign={contextMenu.type === 'node' ? diagramCommands.canAlign : false}
          canDistribute={contextMenu.type === 'node' ? diagramCommands.canDistribute : false}
          onGroupSelection={diagramCommands.groupSelection}
          onUngroupSelection={diagramCommands.ungroupSelection}
          canGroup={contextMenu?.type === 'node' ? diagramCommands.canGroup : false}
          canUngroup={contextMenu?.type === 'node' ? diagramCommands.canUngroup : false}
        />
      )}
      {pendingDeleteNodeId && (
        <div className="pointer-events-none absolute inset-x-0 bottom-4 z-50 flex justify-center">
          <div className="pointer-events-auto flex items-center gap-3 rounded-lg border border-default bg-card px-4 py-2.5 shadow-lg">
            <span className="text-xs text-muted-foreground">Delete this device?</span>
            <button
              onClick={() => setPendingDeleteNodeId(null)}
              className="rounded border border-default px-3 py-1 text-xs text-primary hover:bg-elevated"
            >
              Cancel
            </button>
            <button
              onClick={() => { handleDeleteNode(pendingDeleteNodeId); setPendingDeleteNodeId(null) }}
              className="rounded bg-red-500/20 px-3 py-1 text-xs font-medium text-red-400 hover:bg-red-500/30"
            >
              Delete
            </button>
          </div>
        </div>
      )}
      <input
        ref={drawioImportRef}
        type="file"
        accept=".drawio,.xml"
        className="hidden"
        onChange={handleDrawioFileChange}
      />
      {showShortcuts && (
        <KeyboardShortcutsOverlay onClose={() => setShowShortcuts(false)} />
      )}
    </div>
  )
}

export default function DiagramEditor() {
  return (
    <ReactFlowProvider>
      <DiagramEditorInner />
    </ReactFlowProvider>
  )
}
