/**
 * Canvas page: the graph editor (React Flow) with nodes, edges, context menu, import/export.
 * Rendered inside the platform when a project is selected.
 */

import React, { useCallback, useMemo, useRef } from 'react'
import {
  ReactFlow,
  ReactFlowProvider,
  Controls,
  Background,
  MiniMap,
  addEdge,
  applyNodeChanges,
  applyEdgeChanges,
  useNodesInitialized,
  useReactFlow,
  type Node,
  type Edge,
  type Connection,
  type ColorMode,
  type NodeChange,
  type EdgeChange,
} from '@xyflow/react'
import { AnimatedEdge } from '@/components/base/AnimatedEdge'
import FlowContext from '@/lib/flowContext'
import { useTheme } from '@/lib/themeContext'
import { useGraphStateWithHistory } from '@/hooks/useGraphStateWithHistory'
import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuSeparator,
  ContextMenuSub,
  ContextMenuSubContent,
  ContextMenuSubTrigger,
  ContextMenuTrigger,
  ContextMenuGroup,
} from '@/components/ui/context-menu'
import { CanvasMenubar } from '@/app/canvas/CanvasMenubar'
import { FlowKeyboardShortcuts } from '@/components/base/FlowKeyboardShortcuts'
import {
  Empty,
  EmptyContent,
  EmptyDescription,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} from '@/components/ui/empty'
import { Button } from '@/components/ui/button'
import { ClipboardPaste, FolderOpen, FileStack, CircleDotDashed } from 'lucide-react'
import { DEFAULT_NODE_STYLE, getDefaultDataForType, getNextNodeId } from '@/lib/flowUtils'
import {
  getRegisteredNodeTypes,
  getRegisteredNodeTypeIds,
  getDefaultStyle,
  isConnectionAllowed,
} from '@/lib/nodeRegistry'
import type { AppNode, AppEdge } from '@/lib/nodeTypes'

const SNAP_GRID: [number, number] = [15, 15]
const snapToGrid = (x: number, y: number): { x: number; y: number } => ({
  x: Math.round(x / SNAP_GRID[0]) * SNAP_GRID[0],
  y: Math.round(y / SNAP_GRID[1]) * SNAP_GRID[1],
})

const NODE_GAP = 150
const initialNodes: AppNode[] = [
  {
    id: 'var_001',
    position: { x: 50, y: 100 },
    data: { value: 'Zoe', valueType: 'string' as const },
    type: 'variable',
    style: DEFAULT_NODE_STYLE.variable,
  },
  {
    id: 'cfg_001',
    position: { x: 50 + DEFAULT_NODE_STYLE.variable.width + NODE_GAP, y: 50 },
    data: {
      plantuml: '@startuml\nactor User\nparticipant "{{ var_001 }}" as R\nUser -> R : loves\n@enduml\n',
      title: 'config-cfg_001',
    },
    type: 'config',
    style: DEFAULT_NODE_STYLE.config,
  },
  {
    id: 'rnd_001',
    position: {
      x: 50 + DEFAULT_NODE_STYLE.variable.width + NODE_GAP + DEFAULT_NODE_STYLE.config.width + NODE_GAP,
      y: 50,
    },
    data: {},
    type: 'render',
    style: DEFAULT_NODE_STYLE.render,
  },
]

const initialEdges: AppEdge[] = [
  { id: 'e-var_001-cfg_001', source: 'var_001', target: 'cfg_001', type: 'animated' },
  { id: 'e-cfg_001-rnd_001', source: 'cfg_001', target: 'rnd_001', type: 'animated' },
]

function getExampleGraph(): { nodes: AppNode[]; edges: AppEdge[] } {
  return {
    nodes: initialNodes.map((n) => ({
      ...n,
      data: n.data && typeof n.data === 'object' ? { ...(n.data as object) } : n.data,
    })),
    edges: initialEdges.map((e) => ({ ...e })),
  }
}

const PROJECT_FILE_EXT = '.zui.json'
const PROJECT_VERSION = 1

export type CanvasMessage = { type: 'success' | 'error'; text: string }

function FlowFitViewOnLoad() {
  const nodesInitialized = useNodesInitialized()
  const { fitView } = useReactFlow()
  React.useEffect(() => {
    if (nodesInitialized) fitView?.({ duration: 200 })
  }, [nodesInitialized, fitView])
  return null
}

export type CanvasPageProps = {
  /** Optional project id for future per-project graph loading */
  projectId?: string
}

export function CanvasPage({ projectId: _projectId }: CanvasPageProps) {
  const { theme } = useTheme()
  const {
    nodes,
    edges,
    setNodes,
    setEdges,
    setNodesSilent,
    applyGraph,
    saveForDragEnd,
    commitDragEnd,
    undo,
    redo,
    canUndo,
    canRedo,
    setStateImmediate,
  } = useGraphStateWithHistory(getExampleGraph().nodes, getExampleGraph().edges)

  const importInputRef = useRef<HTMLInputElement | null>(null)
  const [rfInstance, setRfInstance] = React.useState<unknown>(null)
  const [renamingNodeId, setRenamingNodeId] = React.useState<string | null>(null)
  const [connectionFrom, setConnectionFrom] = React.useState<{ nodeId: string; sourceHandle?: string } | null>(null)
  const [canvasMessage, setCanvasMessage] = React.useState<CanvasMessage | null>(null)
  const wrapperRef = useRef<HTMLDivElement | null>(null)
  const lastClickRef = useRef<{ clientX: number; clientY: number } | null>(null)
  const flowActionsRef = useRef<{ pasteAtViewportCenter: () => void; fitView: () => void } | null>(null)
  const [contextTarget, setContextTarget] = React.useState<null | { type: 'canvas'; clientX: number; clientY: number }>(null)
  const [lastCreatedNodeId, setLastCreatedNodeId] = React.useState<string | null>(null)
  const [ariaAnnouncement, setAriaAnnouncement] = React.useState<string | null>(null)
  const nodesRef = useRef(nodes)
  nodesRef.current = nodes

  const [apiTodosCount, setApiTodosCount] = React.useState<number | null>(null)
  React.useEffect(() => {
    fetch('/api/todos')
      .then((r) => r.json())
      .then((data: unknown) => {
        if (Array.isArray(data)) setApiTodosCount(data.length)
      })
      .catch(() => setApiTodosCount(-1))
  }, [])

  const pendingChangesRef = useRef<NodeChange<Node>[]>([])
  const rafRef = useRef<number | null>(null)
  const onNodesChange = useCallback(
    (changes: NodeChange<Node>[]) => {
      if (changes.length === 0) return
      const pending = pendingChangesRef.current
      for (const c of changes) {
        const id = (c as { id?: string }).id
        if (id != null) {
          const i = pending.findIndex((p) => (p as { id?: string }).id === id)
          if (i >= 0) pending[i] = c
          else pending.push(c)
        } else pending.push(c)
      }
      if (rafRef.current === null) {
        rafRef.current = requestAnimationFrame(() => {
          rafRef.current = null
          const toApply = pendingChangesRef.current.splice(0, pendingChangesRef.current.length)
          if (toApply.length > 0) setNodesSilent((nds) => applyNodeChanges(toApply, nds))
        })
      }
    },
    [setNodesSilent]
  )

  const nodeTypes = useMemo(
    () => Object.fromEntries(getRegisteredNodeTypes().map((r) => [r.id, r.component])),
    []
  )
  const edgeTypes = useMemo(() => ({ animated: AnimatedEdge }), [])
  const defaultEdgeOptions = useMemo(() => ({ type: 'animated' as const }), [])

  const onEdgesChange = useCallback(
    (changes: EdgeChange<Edge>[]) => {
      if (changes.length === 0) return
      setEdges((eds) => applyEdgeChanges(changes, eds))
    },
    [setEdges]
  )

  const onConnect = useCallback(
    (params: Connection) => setEdges((eds) => addEdge(params, eds)),
    [setEdges]
  )

  const isValidConnection = useCallback(
    (connection: Connection | AppEdge) => {
      const src = 'source' in connection ? connection.source : undefined
      const tgt = 'target' in connection ? connection.target : undefined
      if (typeof src !== 'string' || typeof tgt !== 'string') return false
      const sourceNode = nodes.find((n) => n.id === src)
      const targetNode = nodes.find((n) => n.id === tgt)
      const sourceType = sourceNode?.type
      const targetType = targetNode?.type
      if (!sourceType || !targetType) return false
      if (sourceType === 'render' && targetType === 'config') {
        const targetData = targetNode?.data as { configType?: string } | undefined
        if (targetData?.configType !== 'markdown') return false
      }
      return isConnectionAllowed(sourceType, targetType, src, tgt)
    },
    [nodes]
  )

  const onConnectStart = useCallback(
    (
      _: React.MouseEvent<Element> | React.TouchEvent<Element> | MouseEvent | TouchEvent,
      params: { nodeId?: string | null; handleId?: string | null; handleType?: string | null }
    ) => {
      if (params.handleType !== 'source' || !params.nodeId) {
        setConnectionFrom(null)
        return
      }
      setConnectionFrom({ nodeId: params.nodeId, sourceHandle: params.handleId ?? undefined })
    },
    []
  )
  const onConnectEnd = useCallback(() => setConnectionFrom(null), [])

  const onInit = useCallback((instance: unknown) => setRfInstance(instance), [])

  React.useEffect(() => {
    if (!canvasMessage) return
    const t = setTimeout(() => setCanvasMessage(null), 3000)
    return () => clearTimeout(t)
  }, [canvasMessage])

  const onNodeDragStart = useCallback(() => saveForDragEnd(), [saveForDragEnd])
  const onNodeDragStop = useCallback(() => commitDragEnd(), [commitDragEnd])

  const handleExportProject = useCallback(() => {
    const state = { version: PROJECT_VERSION, nodes, edges }
    const blob = new Blob([JSON.stringify(state, null, 2)], { type: 'application/json' })
    const url = URL.createObjectURL(blob)
    const a = document.createElement('a')
    a.href = url
    a.download = `project${PROJECT_FILE_EXT}`
    a.click()
    URL.revokeObjectURL(url)
    setCanvasMessage({ type: 'success', text: 'Project exported' })
  }, [nodes, edges])

  const handleImportProject = useCallback(() => importInputRef.current?.click(), [])

  const handleLoadExample = useCallback(() => {
    const { nodes: exampleNodes, edges: exampleEdges } = getExampleGraph()
    setStateImmediate({ nodes: exampleNodes, edges: exampleEdges })
    setCanvasMessage({ type: 'success', text: 'Example loaded' })
  }, [setStateImmediate])

  const onImportFileChange = useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => {
      const file = e.target.files?.[0]
      e.target.value = ''
      if (!file) return
      const reader = new FileReader()
      reader.onload = () => {
        try {
          const text = reader.result as string
          const state = JSON.parse(text) as { version?: number; nodes?: unknown[]; edges?: unknown[] }
          if (!state || !Array.isArray(state.nodes) || !Array.isArray(state.edges)) {
            setCanvasMessage({ type: 'error', text: 'Invalid file: expected nodes and edges arrays' })
            return
          }
          setStateImmediate({ nodes: state.nodes as AppNode[], edges: state.edges as AppEdge[] })
          setCanvasMessage(
            state.version != null && state.version > PROJECT_VERSION
              ? { type: 'error', text: 'Project was created with a newer app version' }
              : { type: 'success', text: 'Project loaded' }
          )
        } catch {
          setCanvasMessage({ type: 'error', text: 'Invalid file: not valid JSON' })
        }
      }
      reader.readAsText(file)
    },
    [setStateImmediate]
  )

  const flowContextValue = useMemo(
    () => ({
      nodes,
      setNodes,
      edges,
      setEdges,
      renamingNodeId,
      setRenamingNodeId,
      connectionFrom,
      setConnectionFrom,
      isValidConnection,
      flowActionsRef,
    }),
    [
      nodes,
      setNodes,
      edges,
      setEdges,
      renamingNodeId,
      setRenamingNodeId,
      connectionFrom,
      setConnectionFrom,
      isValidConnection,
      flowActionsRef,
    ]
  )

  const onContextMenuCapture = useCallback((ev: React.MouseEvent) => {
    const target = ev.target as HTMLElement
    if (target.closest('.react-flow__node')) {
      ev.preventDefault()
      ev.stopPropagation()
    }
  }, [])

  const onWheelCapture = useCallback((ev: React.WheelEvent) => {
    const target = ev.target as HTMLElement
    if (target.closest('.react-flow__node')) {
      ev.preventDefault()
      ev.stopPropagation()
    }
  }, [])

  const onCanvasContextMenu = useCallback((ev: React.MouseEvent) => {
    ev.preventDefault()
    lastClickRef.current = { clientX: ev.clientX, clientY: ev.clientY }
    setContextTarget({ type: 'canvas', clientX: ev.clientX, clientY: ev.clientY })
  }, [])

  const getMenuPosition = useCallback(() => {
    if (!rfInstance) return null
    const click = contextTarget ?? lastClickRef.current
    const clientX = click?.clientX ?? window.innerWidth / 2
    const clientY = click?.clientY ?? window.innerHeight / 2
    try {
      const inst = rfInstance as { screenToFlowPosition?: (p: { x: number; y: number }) => { x: number; y: number }; project?: (p: { x: number; y: number }) => { x: number; y: number } }
      const screenToFlow = inst.screenToFlowPosition ?? inst.project
      const p = screenToFlow?.call(rfInstance, { x: clientX, y: clientY })
      return p ? snapToGrid(p.x, p.y) : null
    } catch {
      return snapToGrid(clientX, clientY)
    }
  }, [rfInstance, contextTarget])

  const createNode = useCallback(
    (type: string) => {
      const position = getMenuPosition()
      if (position == null || typeof type !== 'string') return
      const existingIds = nodesRef.current.map((n) => n.id).filter((id): id is string => id != null)
      const newId = getNextNodeId(type, existingIds)
      const dataMap = getDefaultDataForType(type, newId)
      const style = getDefaultStyle(type)
      const newNode: Node = {
        id: newId,
        type: type as Node['type'],
        position: { x: position.x, y: position.y },
        data: dataMap,
        style,
      }
      setNodes((nds) => nds.concat(newNode))
      setLastCreatedNodeId(newId)
      lastClickRef.current = null
      setContextTarget(null)
    },
    [getMenuPosition, setNodes]
  )

  React.useEffect(() => {
    if (lastCreatedNodeId == null) return
    const id = lastCreatedNodeId
    const raf = requestAnimationFrame(() => {
      const nodeEl = document.querySelector(`.react-flow__node[data-id="${id}"]`) as HTMLElement | null
      if (nodeEl) {
        setAriaAnnouncement('Node created')
        nodeEl.setAttribute('tabindex', '-1')
        nodeEl.focus({ preventScroll: false })
        setTimeout(() => setAriaAnnouncement(null), 1000)
      }
      setLastCreatedNodeId(null)
    })
    return () => cancelAnimationFrame(raf)
  }, [lastCreatedNodeId, nodes])

  const pasteNode = useCallback(async () => {
    const position = getMenuPosition()
    if (position == null) 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 nodeType = raw.type
      setNodes((nds) => {
        const existingIds = nds.map((n) => n.id).filter((id): id is string => id != null)
        const newId = getNextNodeId(nodeType, existingIds)
        const data = raw.data != null && typeof raw.data === 'object' ? { ...raw.data } : {}
        if (nodeType === 'config' && data && 'title' in data) data.title = `config-${newId}`
        const style = getDefaultStyle(nodeType)
        const newNode: Node = {
          id: newId,
          type: nodeType as Node['type'],
          position: { x: position.x, y: position.y },
          data,
          style,
        }
        return nds.concat(newNode)
      })
      lastClickRef.current = null
      setContextTarget(null)
    } catch {
      /* ignore */
    }
  }, [getMenuPosition, setNodes])

  const deleteNode = useCallback(
    (id: string | undefined) => {
      if (!id) return
      applyGraph(({ nodes: nds, edges: eds }) => ({
        nodes: nds.filter((n) => n.id !== id),
        edges: eds.filter((e) => e.source !== id && e.target !== id),
      }))
      setContextTarget(null)
    },
    [applyGraph]
  )

  return (
    <div
      className="reactflow-wrapper flex flex-col h-full"
      ref={wrapperRef}
      onContextMenuCapture={onContextMenuCapture}
      onContextMenu={onCanvasContextMenu}
      style={{ position: 'relative' }}
    >
      <div role="status" aria-live="polite" aria-atomic className="sr-only">
        {ariaAnnouncement}
      </div>
      <input
        ref={importInputRef}
        type="file"
        accept=".json,.zui.json,application/json"
        className="hidden"
        onChange={onImportFileChange}
        aria-hidden
      />
      <CanvasMenubar
        onImport={handleImportProject}
        onExport={handleExportProject}
        undo={undo}
        redo={redo}
        canUndo={canUndo}
        canRedo={canRedo}
        onFitView={() => flowActionsRef.current?.fitView?.()}
      />
      {apiTodosCount !== null && (
        <div className="shrink-0 px-3 py-1 text-xs text-muted-foreground border-b border-border/50">
          Backend API: {apiTodosCount >= 0 ? `${apiTodosCount} todos` : 'unavailable'}
        </div>
      )}
      {canvasMessage && (
        <div
          role="status"
          className={`shrink-0 px-3 py-2 text-sm ${canvasMessage.type === 'success'
            ? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-200'
            : 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-200'
            }`}
        >
          {canvasMessage.text}
        </div>
      )}
      <div className="flex-1 min-h-0 relative flex flex-col">
        <div className="flex-1 min-h-0 flex flex-col">
          <FlowContext.Provider value={flowContextValue}>
            <ContextMenu>
              <ContextMenuTrigger asChild>
                <div
                  className="flex-1 min-h-0 w-full relative"
                  onWheelCapture={onWheelCapture}
                  role="application"
                  aria-label="Graph canvas"
                  tabIndex={0}
                >
                  {nodes.length === 0 && (
                    <div className="absolute inset-0 flex items-center justify-center pointer-events-none z-10">
                      <div className="pointer-events-auto rounded-lg border border-border bg-background/95 dark:bg-background/90 shadow-sm">
                        <Empty className="p-6 md:p-8">
                          <EmptyHeader>
                            <EmptyMedia variant="icon">
                              <CircleDotDashed className="size-6" />
                            </EmptyMedia>
                            <EmptyTitle>Start adding a new node!</EmptyTitle>
                            <EmptyDescription>
                              Right‑click to add nodes. <br />
                              Import a project or paste a node.
                            </EmptyDescription>
                          </EmptyHeader>
                          <EmptyContent className="flex-row flex-wrap justify-center gap-2">
                            <Button onClick={handleImportProject} variant="outline" size="sm">
                              <FolderOpen className="size-4" />
                              Import…
                            </Button>
                            <Button onClick={handleLoadExample} variant="outline" size="sm">
                              <FileStack className="size-4" />
                              Load example
                            </Button>
                          </EmptyContent>
                        </Empty>
                      </div>
                    </div>
                  )}
                  <ReactFlowProvider initialNodes={nodes} initialEdges={edges} fitView>
                    <FlowFitViewOnLoad />
                    <FlowKeyboardShortcuts />
                    <ReactFlow
                      nodes={nodes}
                      edges={edges}
                      onNodesChange={onNodesChange}
                      onEdgesChange={onEdgesChange}
                      onConnect={onConnect}
                      onConnectStart={onConnectStart}
                      onConnectEnd={onConnectEnd}
                      onNodeDragStart={onNodeDragStart}
                      onNodeDragStop={onNodeDragStop}
                      isValidConnection={isValidConnection}
                      nodeTypes={nodeTypes}
                      edgeTypes={edgeTypes}
                      defaultEdgeOptions={defaultEdgeOptions}
                      colorMode={theme as ColorMode}
                      snapToGrid
                      snapGrid={SNAP_GRID}
                      fitView
                      onInit={onInit}
                      nodeDragThreshold={1}
                      nodesDraggable
                      nodesConnectable
                      elementsSelectable
                    >
                      {/* BackgroundVariant from @xyflow/system expects enum; 'dots' is valid at runtime */}
                      <Background variant={'dots' as React.ComponentProps<typeof Background>['variant']} gap={20} />
                      <div role="group" aria-label="Canvas controls: zoom and fit view">
                        <Controls />
                      </div>
                      <div role="region" aria-label="Minimap: overview of the graph">
                        <MiniMap />
                      </div>
                    </ReactFlow>
                  </ReactFlowProvider>
                </div>
              </ContextMenuTrigger>
              <ContextMenuContent className="w-48" aria-label="Canvas menu: create node or paste">
                <ContextMenuGroup>
                  <ContextMenuSub>
                    <ContextMenuSubTrigger>Create Node</ContextMenuSubTrigger>
                    <ContextMenuSubContent className="w-44">
                      <ContextMenuGroup>
                        {getRegisteredNodeTypes().map((desc, index) => (
                          <React.Fragment key={desc.id}>
                            {index === 2 ? <ContextMenuSeparator /> : null}
                            <ContextMenuItem onSelect={() => createNode(desc.id)}>
                              {desc.menuIcon}
                              {desc.menuLabel}
                            </ContextMenuItem>
                          </React.Fragment>
                        ))}
                      </ContextMenuGroup>
                    </ContextMenuSubContent>
                  </ContextMenuSub>
                  <ContextMenuSeparator />
                  <ContextMenuItem onSelect={() => pasteNode()}>
                    <ClipboardPaste className="mr-2 h-4 w-4" />
                    Paste
                  </ContextMenuItem>
                </ContextMenuGroup>
              </ContextMenuContent>
            </ContextMenu>
          </FlowContext.Provider>
        </div>
      </div>
    </div>
  )
}
