import { useState, useCallback, useEffect, useRef } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import {
  ReactFlowProvider,
  useNodesState,
  useEdgesState,
  addEdge,
  useReactFlow,
  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 { DiagramHeader } 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 { networkDiagramsApi, deviceTypesApi } from '@/api'
import { toast } from '@/lib/toast'
import type { DeviceTypeResponse, DeviceProperties, AIGenerateResponse, DiagramEdge, DiagramNode } from '@/types'
import type { DeviceNodeData } from '@/components/network/nodes/DeviceNode'

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 canvasRef = useRef<HTMLDivElement | null>(null)
  const [contextMenu, setContextMenu] = useState<ContextMenuState>(null)

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

  const {
    copyNodes,
    pasteNodes,
    duplicateNodes,
    selectAll,
    deleteSelected,
    hasClipboard,
  } = useCanvasShortcuts({
    nodes,
    edges,
    setNodes,
    setEdges,
    setIsDirty: (v: boolean) => setIsDirty(v),
    canvasRef,
  })

  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,
              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,
            },
          }))
        )
        setLastSavedAt(new Date(diagram.updated_at))
      } catch {
        toast.error('Failed to load diagram')
        navigate('/network-diagrams')
      } finally {
        if (!cancelled) setLoading(false)
      }
    })()
    return () => { cancelled = true }
  }, [id, navigate, setNodes, setEdges])

  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
      return {
        id: n.id,
        type: data.deviceType,
        label: data.label,
        position: n.position,
        properties: data.properties,
      }
    })
  }, [getNodes])

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

  const handleSave = useCallback(async () => {
    setIsSaving(true)
    try {
      const payload = {
        name,
        client_name: clientName,
        asset_name: assetName,
        description,
        nodes: serializeNodes(),
        edges: serializeEdges(),
      }
      if (diagramIdRef.current) {
        await networkDiagramsApi.update(diagramIdRef.current, payload)
      } else {
        const created = await networkDiagramsApi.create(payload)
        setDiagramId(created.id)
        navigate(`/network-diagrams/${created.id}`, { replace: true })
      }
      setIsDirty(false)
      setLastSavedAt(new Date())
    } catch {
      toast.error('Failed to save diagram')
    } finally {
      setIsSaving(false)
    }
  }, [name, clientName, assetName, description, serializeNodes, serializeEdges, navigate])

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

  const onConnect = useCallback((connection: Connection) => {
    setEdges(eds => addEdge({
      ...connection,
      type: 'connection',
      data: { connectionType: 'ethernet' },
    }, eds))
    setIsDirty(true)
  }, [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()
    setContextMenu({
      type: 'canvas',
      position: { x: event.clientX, y: event.clientY },
    })
  }, [])

  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,
        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,
      }
      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,
        },
      }
      setNodes(nds => [...nds, newNode])
      setIsDirty(true)
    }
  }, [setNodes, screenToFlowPosition])

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

  const handleEdgeUpdate = useCallback((edgeId: string, updates: Partial<DiagramEdge>) => {
    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 } : {}),
        },
      }
    }))
    setIsDirty(true)
  }, [setEdges])

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

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

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

  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 },
    }))

    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)
  }, [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(() => {
    toast.info('PNG export — use your browser\'s screenshot tool or Print > Save as Image for now')
  }, [])

  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])

  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}
        isSaving={isSaving}
        lastSavedAt={lastSavedAt}
        diagramId={diagramId}
        onNameChange={n => { setName(n); setIsDirty(true) }}
        onSave={handleSave}
        onExportPng={handleExportPng}
        onExportPdf={handleExportPdf}
        onExportJson={handleExportJson}
      />
      <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="flex-1 min-h-0" ref={canvasRef}>
            <NetworkCanvas
              nodes={nodes}
              edges={edges}
              onNodesChange={handleNodesChange}
              onEdgesChange={handleEdgesChange}
              onConnect={onConnect}
              onNodeSelect={setSelectedNodeId}
              onEdgeSelect={setSelectedEdgeId}
              onDrop={onDrop}
              onDragOver={onDragOver}
              onDragLeave={onDragLeave}
              isDragOver={isDragOver}
              onNodeContextMenu={handleNodeContextMenu}
              onPaneContextMenu={handlePaneContextMenu}
              onPaneClick={closeContextMenu}
            />
          </div>
          <AIAssistPanel
            onGenerate={handleAIGenerate}
            getExistingBounds={getExistingBounds}
            hasNodes={nodes.length > 0}
          />
        </div>
        <PropertiesPanel
          selectedNode={selectedNode}
          selectedEdge={selectedEdge}
          onNodeUpdate={handleNodeUpdate}
          onEdgeUpdate={handleEdgeUpdate}
          onEdgeTypeChange={handleEdgeTypeChange}
          onDeleteNode={handleDeleteNode}
          onDeleteEdge={handleDeleteEdge}
        />
      </div>
      {contextMenu && (
        <ContextMenu
          position={contextMenu.position}
          actions={
            contextMenu.type === 'node'
              ? getNodeMenuActions({
                  onCopy: copyNodes,
                  onDuplicate: duplicateNodes,
                  onDelete: deleteSelected,
                })
              : getCanvasMenuActions({
                  onPaste: pasteNodes,
                  onSelectAll: selectAll,
                  onFitView: () => fitView({ padding: 0.2 }),
                  hasClipboard: hasClipboard(),
                })
          }
          onClose={closeContextMenu}
        />
      )}
    </div>
  )
}

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