import React, { useCallback, useContext, useEffect } from 'react'
import { useReactFlow } from '@xyflow/react'
import type { Node } from '@xyflow/react'
import FlowContext from '@/lib/flowContext'
import { getNextNodeId, getDefaultDataForType } from '@/lib/flowUtils'
import { getDefaultStyle, getRegisteredNodeTypeIds } from '@/lib/nodeRegistry'

const DUPLICATE_OFFSET = { x: 30, y: 30 }

function isMod(ev: KeyboardEvent) {
  return ev.ctrlKey || ev.metaKey
}

/** Must be rendered inside ReactFlowProvider and FlowContext. Handles Escape, ⌘C, ⌘V, ⌘D, ⌘0. */
export function FlowKeyboardShortcuts() {
  const { fitView, screenToFlowPosition } = useReactFlow()
  const ctx = useContext(FlowContext)
  const nodes = ctx?.nodes ?? []
  const setNodes = ctx?.setNodes
  const setConnectionFrom = ctx?.setConnectionFrom
  const flowActionsRef = ctx?.flowActionsRef

  const pasteAtViewportCenter = useCallback(async () => {
    if (!setNodes || !screenToFlowPosition) return
    try {
      const text = await navigator.clipboard?.readText()
      if (!text) return
      const raw = JSON.parse(text) as {
        id?: string
        type?: string
        data?: Record<string, unknown>
        position?: { x: number; y: number }
        style?: unknown
      }
      const validIds = getRegisteredNodeTypeIds()
      if (!raw || typeof raw.type !== 'string' || !validIds.includes(raw.type)) return
      const pane = document.querySelector('.react-flow__viewport')
      const rect = pane?.getBoundingClientRect()
      const center = rect
        ? { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }
        : { x: window.innerWidth / 2, y: window.innerHeight / 2 }
      const position = screenToFlowPosition(center)
      setNodes((nds: Node[]) => {
        const newId = getNextNodeId(raw.type, nds.map((n) => n.id))
        const data: Record<string, unknown> =
          raw.data != null && typeof raw.data === 'object'
            ? { ...raw.data }
            : (getDefaultDataForType(raw.type, newId) as Record<string, unknown>)
        if (raw.type === 'config') data.title = `config-${newId}`
        const style = getDefaultStyle(raw.type)
        const newNode: Node = {
          id: newId,
          type: raw.type as Node['type'],
          position: { x: position.x, y: position.y },
          data,
          style,
        }
        return nds.concat(newNode)
      })
    } catch {
      // Invalid clipboard or not a copied node
    }
  }, [setNodes, screenToFlowPosition])

  const doFitView = useCallback(() => {
    fitView?.({ duration: 200 })
  }, [fitView])

  useEffect(() => {
    if (flowActionsRef) {
      flowActionsRef.current = { pasteAtViewportCenter, fitView: doFitView }
      return () => {
        flowActionsRef.current = null
      }
    }
  }, [flowActionsRef, pasteAtViewportCenter, doFitView])

  useEffect(() => {
    const onKeyDown = (ev: KeyboardEvent) => {
      if (ev.key === 'Escape') {
        setConnectionFrom?.(null)
        setNodes?.((nds) => nds.map((n) => ({ ...n, selected: false })))
        ev.preventDefault()
        return
      }
      if (ev.key === 'c' && isMod(ev) && !ev.shiftKey) {
        const selectedNodes = nodes.filter((n) => (n as Node & { selected?: boolean }).selected)
        if (selectedNodes.length === 1) {
          const node = selectedNodes[0] as Node & { selected?: boolean }
          const copy = {
            id: node.id,
            type: node.type,
            data: node.data,
            position: node.position,
            style: node.style,
          }
          navigator.clipboard?.writeText(JSON.stringify(copy)).catch(() => {})
          ev.preventDefault()
        }
        return
      }
      if (ev.key === 'v' && isMod(ev) && !ev.shiftKey) {
        pasteAtViewportCenter()
        ev.preventDefault()
        return
      }
      if (ev.key === 'd' && isMod(ev) && !ev.shiftKey) {
        const selectedNodes = nodes.filter((n) => (n as Node & { selected?: boolean }).selected)
        if (selectedNodes.length > 0 && setNodes) {
          setNodes((nds: Node[]) => {
            const existingIds = nds.map((n) => n.id)
            const toAdd: Node[] = []
            for (const node of selectedNodes) {
              const n = node as Node & { selected?: boolean }
              const pos = n.position ?? { x: 0, y: 0 }
              const newId = getNextNodeId(String(n.type), [...existingIds, ...toAdd.map((x) => x.id)])
              existingIds.push(newId)
              const newNode: Node = {
                id: newId,
                type: n.type,
                position: { x: pos.x + DUPLICATE_OFFSET.x, y: pos.y + DUPLICATE_OFFSET.y },
                data: typeof n.data === 'object' && n.data !== null ? { ...(n.data as object) } : n.data,
                style: getDefaultStyle(String(n.type)),
              }
              if (
                newNode.data &&
                typeof newNode.data === 'object' &&
                'title' in newNode.data &&
                String(n.type) === 'config'
              ) {
                ;(newNode.data as Record<string, unknown>).title = `${newId}`
              }
              toAdd.push(newNode)
            }
            return nds.concat(toAdd)
          })
          ev.preventDefault()
        }
        return
      }
      if (ev.key === '0' && isMod(ev) && !ev.shiftKey) {
        doFitView()
        ev.preventDefault()
        return
      }
    }
    window.addEventListener('keydown', onKeyDown, true)
    return () => window.removeEventListener('keydown', onKeyDown, true)
  }, [
    nodes,
    setNodes,
    setConnectionFrom,
    pasteAtViewportCenter,
    doFitView,
  ])

  return null
}
