import type { CreateSnippetDialogPayload } from './create-snippet-dialog'
import type { Edge, Node } from './types'
import type { SnippetCanvasData } from '@/models/snippet'
import { produce } from 'immer'
import {
  memo,
  useCallback,
  useEffect,
  useMemo,
  useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import { useStore as useReactFlowStore, useStoreApi } from 'reactflow'
import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuSeparator,
} from '@/app/components/base/ui/context-menu'
import { toast } from '@/app/components/base/ui/toast'
import { useSnippetAndEvaluationPlanAccess } from '@/hooks/use-snippet-and-evaluation-plan-access'
import { useRouter } from '@/next/navigation'
import { consoleClient } from '@/service/client'
import { useCreateSnippetMutation } from '@/service/use-snippets'
import { cn } from '@/utils/classnames'
import CreateSnippetDialog from './create-snippet-dialog'
import { useNodesInteractions, useNodesReadOnly, useNodesSyncDraft } from './hooks'
import { useSelectionInteractions } from './hooks/use-selection-interactions'
import { useWorkflowHistory, WorkflowHistoryEvent } from './hooks/use-workflow-history'
import ShortcutsName from './shortcuts-name'
import { useStore, useWorkflowStore } from './store'
import { BlockEnum, TRIGGER_NODE_TYPES } from './types'

const AlignType = {
  Bottom: 'bottom',
  Center: 'center',
  DistributeHorizontal: 'distributeHorizontal',
  DistributeVertical: 'distributeVertical',
  Left: 'left',
  Middle: 'middle',
  Right: 'right',
  Top: 'top',
} as const

type AlignTypeValue = (typeof AlignType)[keyof typeof AlignType]

type AlignBounds = {
  minX: number
  maxX: number
  minY: number
  maxY: number
}

type AlignMenuItem = {
  alignType: AlignTypeValue
  icon: string
  iconClassName?: string
  translationKey: string
}

type ActionMenuItem = {
  action: 'copy' | 'createSnippet' | 'delete' | 'duplicate'
  disabled?: boolean
  shortcutKeys?: string[]
  translationKey: string
}

const DEFAULT_SNIPPET_VIEWPORT: SnippetCanvasData['viewport'] = { x: 0, y: 0, zoom: 1 }

const alignMenuItems: AlignMenuItem[] = [
  { alignType: AlignType.Left, icon: 'i-ri-align-item-left-line', translationKey: 'operator.alignLeft' },
  { alignType: AlignType.Center, icon: 'i-ri-align-item-horizontal-center-line', translationKey: 'operator.alignCenter' },
  { alignType: AlignType.Right, icon: 'i-ri-align-item-right-line', translationKey: 'operator.alignRight' },
  { alignType: AlignType.Top, icon: 'i-ri-align-item-top-line', translationKey: 'operator.alignTop' },
  { alignType: AlignType.Middle, icon: 'i-ri-align-item-vertical-center-line', iconClassName: 'rotate-90', translationKey: 'operator.alignMiddle' },
  { alignType: AlignType.Bottom, icon: 'i-ri-align-item-bottom-line', translationKey: 'operator.alignBottom' },
  { alignType: AlignType.DistributeHorizontal, icon: 'i-ri-align-justify-line', translationKey: 'operator.distributeHorizontal' },
  { alignType: AlignType.DistributeVertical, icon: 'i-ri-align-justify-line', iconClassName: 'rotate-90', translationKey: 'operator.distributeVertical' },
]

const getAlignableNodes = (nodes: Node[], selectedNodes: Node[]) => {
  const selectedNodeIds = new Set(selectedNodes.map(node => node.id))
  const childNodeIds = new Set<string>()

  nodes.forEach((node) => {
    if (!node.data._children?.length || !selectedNodeIds.has(node.id))
      return

    node.data._children.forEach((child) => {
      childNodeIds.add(child.nodeId)
    })
  })

  return nodes.filter(node => selectedNodeIds.has(node.id) && !childNodeIds.has(node.id))
}

const getAlignBounds = (nodes: Node[]): AlignBounds | null => {
  const validNodes = nodes.filter(node => node.width && node.height)
  if (validNodes.length <= 1)
    return null

  return validNodes.reduce<AlignBounds>((bounds, node) => {
    const width = node.width!
    const height = node.height!

    return {
      minX: Math.min(bounds.minX, node.position.x),
      maxX: Math.max(bounds.maxX, node.position.x + width),
      minY: Math.min(bounds.minY, node.position.y),
      maxY: Math.max(bounds.maxY, node.position.y + height),
    }
  }, {
    minX: Number.MAX_SAFE_INTEGER,
    maxX: Number.MIN_SAFE_INTEGER,
    minY: Number.MAX_SAFE_INTEGER,
    maxY: Number.MIN_SAFE_INTEGER,
  })
}

const alignNodePosition = (
  currentNode: Node,
  nodeToAlign: Node,
  alignType: AlignTypeValue,
  bounds: AlignBounds,
) => {
  const width = nodeToAlign.width ?? 0
  const height = nodeToAlign.height ?? 0

  switch (alignType) {
    case AlignType.Left:
      currentNode.position.x = bounds.minX
      if (currentNode.positionAbsolute)
        currentNode.positionAbsolute.x = bounds.minX
      break
    case AlignType.Center: {
      const centerX = bounds.minX + (bounds.maxX - bounds.minX) / 2 - width / 2
      currentNode.position.x = centerX
      if (currentNode.positionAbsolute)
        currentNode.positionAbsolute.x = centerX
      break
    }
    case AlignType.Right: {
      const rightX = bounds.maxX - width
      currentNode.position.x = rightX
      if (currentNode.positionAbsolute)
        currentNode.positionAbsolute.x = rightX
      break
    }
    case AlignType.Top:
      currentNode.position.y = bounds.minY
      if (currentNode.positionAbsolute)
        currentNode.positionAbsolute.y = bounds.minY
      break
    case AlignType.Middle: {
      const middleY = bounds.minY + (bounds.maxY - bounds.minY) / 2 - height / 2
      currentNode.position.y = middleY
      if (currentNode.positionAbsolute)
        currentNode.positionAbsolute.y = middleY
      break
    }
    case AlignType.Bottom: {
      const bottomY = Math.round(bounds.maxY - height)
      currentNode.position.y = bottomY
      if (currentNode.positionAbsolute)
        currentNode.positionAbsolute.y = bottomY
      break
    }
  }
}

const distributeNodes = (
  nodesToAlign: Node[],
  nodes: Node[],
  alignType: AlignTypeValue,
) => {
  const isHorizontal = alignType === AlignType.DistributeHorizontal
  const sortedNodes = [...nodesToAlign].sort((a, b) =>
    isHorizontal ? a.position.x - b.position.x : a.position.y - b.position.y)

  if (sortedNodes.length < 3)
    return null

  const firstNode = sortedNodes[0]
  const lastNode = sortedNodes[sortedNodes.length - 1]

  const totalGap = isHorizontal
    ? lastNode.position.x + (lastNode.width || 0) - firstNode.position.x
    : lastNode.position.y + (lastNode.height || 0) - firstNode.position.y

  const fixedSpace = sortedNodes.reduce((sum, node) =>
    sum + (isHorizontal ? (node.width || 0) : (node.height || 0)), 0)

  const spacing = (totalGap - fixedSpace) / (sortedNodes.length - 1)
  if (spacing <= 0)
    return null

  return produce(nodes, (draft) => {
    let currentPosition = isHorizontal
      ? firstNode.position.x + (firstNode.width || 0)
      : firstNode.position.y + (firstNode.height || 0)

    for (let index = 1; index < sortedNodes.length - 1; index++) {
      const nodeToAlign = sortedNodes[index]
      const currentNode = draft.find(node => node.id === nodeToAlign.id)
      if (!currentNode)
        continue

      if (isHorizontal) {
        const nextX = currentPosition + spacing
        currentNode.position.x = nextX
        if (currentNode.positionAbsolute)
          currentNode.positionAbsolute.x = nextX
        currentPosition = nextX + (nodeToAlign.width || 0)
      }
      else {
        const nextY = currentPosition + spacing
        currentNode.position.y = nextY
        if (currentNode.positionAbsolute)
          currentNode.positionAbsolute.y = nextY
        currentPosition = nextY + (nodeToAlign.height || 0)
      }
    }
  })
}

const getSelectedSnippetGraph = (
  nodes: Node[],
  edges: Edge[],
  selectedNodes: Node[],
): SnippetCanvasData => {
  const includedNodeIds = new Set(selectedNodes.map(node => node.id))

  let shouldExpand = true
  while (shouldExpand) {
    shouldExpand = false

    nodes.forEach((node) => {
      if (!includedNodeIds.has(node.id))
        return

      if (node.parentId && !includedNodeIds.has(node.parentId)) {
        includedNodeIds.add(node.parentId)
        shouldExpand = true
      }

      node.data._children?.forEach((child) => {
        if (!includedNodeIds.has(child.nodeId)) {
          includedNodeIds.add(child.nodeId)
          shouldExpand = true
        }
      })
    })
  }

  const rootNodes = nodes.filter(node => includedNodeIds.has(node.id) && (!node.parentId || !includedNodeIds.has(node.parentId)))
  const minRootX = rootNodes.length ? Math.min(...rootNodes.map(node => node.position.x)) : 0
  const minRootY = rootNodes.length ? Math.min(...rootNodes.map(node => node.position.y)) : 0

  return {
    nodes: nodes
      .filter(node => includedNodeIds.has(node.id))
      .map((node) => {
        const isRootNode = !node.parentId || !includedNodeIds.has(node.parentId)
        const nextPosition = isRootNode
          ? { x: node.position.x - minRootX, y: node.position.y - minRootY }
          : node.position

        return {
          ...node,
          position: nextPosition,
          positionAbsolute: node.positionAbsolute
            ? (isRootNode
                ? {
                    x: node.positionAbsolute.x - minRootX,
                    y: node.positionAbsolute.y - minRootY,
                  }
                : node.positionAbsolute)
            : undefined,
          selected: false,
          data: {
            ...node.data,
            selected: false,
            _children: node.data._children?.filter(child => includedNodeIds.has(child.nodeId)),
          },
        }
      }),
    edges: edges
      .filter(edge => includedNodeIds.has(edge.source) && includedNodeIds.has(edge.target))
      .map(edge => ({
        ...edge,
        selected: false,
      })),
    viewport: DEFAULT_SNIPPET_VIEWPORT,
  }
}

const SelectionContextmenu = () => {
  const { t } = useTranslation()
  const { push } = useRouter()
  const { canAccess: canAccessSnippetsAndEvaluation } = useSnippetAndEvaluationPlanAccess()
  const createSnippetMutation = useCreateSnippetMutation()
  const { getNodesReadOnly } = useNodesReadOnly()
  const { handleNodesCopy, handleNodesDelete, handleNodesDuplicate } = useNodesInteractions()
  const { handleSelectionContextmenuCancel } = useSelectionInteractions()
  const selectionMenu = useStore(s => s.selectionMenu)
  const [isCreateSnippetDialogOpen, setIsCreateSnippetDialogOpen] = useState(false)
  const [isCreatingSnippet, setIsCreatingSnippet] = useState(false)
  const [selectedGraphSnapshot, setSelectedGraphSnapshot] = useState<SnippetCanvasData | undefined>()

  // Access React Flow methods
  const store = useStoreApi()
  const workflowStore = useWorkflowStore()
  const selectedNodes = useReactFlowStore(state =>
    state.getNodes().filter(node => node.selected),
  )
  const { handleSyncWorkflowDraft } = useNodesSyncDraft()
  const { saveStateToHistory } = useWorkflowHistory()

  const anchor = useMemo(() => {
    if (!selectionMenu)
      return undefined

    return {
      getBoundingClientRect: () => DOMRect.fromRect({
        width: 0,
        height: 0,
        x: selectionMenu.clientX,
        y: selectionMenu.clientY,
      }),
    }
  }, [selectionMenu])

  useEffect(() => {
    if (selectionMenu && selectedNodes.length <= 1)
      handleSelectionContextmenuCancel()
  }, [selectionMenu, selectedNodes.length, handleSelectionContextmenuCancel])

  const isAddToSnippetDisabled = useMemo(() => {
    return selectedNodes.some(node =>
      node.data.type === BlockEnum.Start
      || node.data.type === BlockEnum.End
      || node.data.type === BlockEnum.HumanInput
      || TRIGGER_NODE_TYPES.includes(node.data.type as typeof TRIGGER_NODE_TYPES[number]))
  }, [selectedNodes])

  const handleOpenCreateSnippetDialog = useCallback(() => {
    if (!canAccessSnippetsAndEvaluation || isAddToSnippetDisabled)
      return

    const nodes = store.getState().getNodes()
    const { edges } = store.getState()

    setSelectedGraphSnapshot(getSelectedSnippetGraph(nodes, edges, selectedNodes))
    setIsCreateSnippetDialogOpen(true)
    handleSelectionContextmenuCancel()
  }, [canAccessSnippetsAndEvaluation, handleSelectionContextmenuCancel, isAddToSnippetDisabled, selectedNodes, store])

  const handleCloseCreateSnippetDialog = useCallback(() => {
    setIsCreateSnippetDialogOpen(false)
    setSelectedGraphSnapshot(undefined)
  }, [])

  const handleCreateSnippet = useCallback(async ({
    name,
    description,
    icon,
    graph,
  }: CreateSnippetDialogPayload) => {
    setIsCreatingSnippet(true)

    try {
      const snippet = await createSnippetMutation.mutateAsync({
        body: {
          name,
          description: description || undefined,
          icon_info: {
            icon: icon.type === 'emoji' ? icon.icon : icon.fileId,
            icon_type: icon.type,
            icon_background: icon.type === 'emoji' ? icon.background : undefined,
            icon_url: icon.type === 'image' ? icon.url : undefined,
          },
        },
      })

      await consoleClient.snippets.syncDraftWorkflow({
        params: { snippetId: snippet.id },
        body: { graph },
      })

      toast.success(t('snippet.createSuccess', { ns: 'workflow' }))
      handleCloseCreateSnippetDialog()
      push(`/snippets/${snippet.id}/orchestrate`)
    }
    catch (error) {
      toast.error(error instanceof Error ? error.message : t('createFailed', { ns: 'snippet' }))
    }
    finally {
      setIsCreatingSnippet(false)
    }
  }, [createSnippetMutation, handleCloseCreateSnippetDialog, push, t])

  const menuActions = useMemo<ActionMenuItem[]>(() => {
    const nextActions: ActionMenuItem[] = []

    if (canAccessSnippetsAndEvaluation) {
      nextActions.push({
        action: 'createSnippet',
        disabled: isAddToSnippetDisabled,
        translationKey: 'snippet.addToSnippet',
      })
    }

    nextActions.push(
      {
        action: 'copy',
        shortcutKeys: ['ctrl', 'c'],
        translationKey: 'common.copy',
      },
      {
        action: 'duplicate',
        shortcutKeys: ['ctrl', 'd'],
        translationKey: 'common.duplicate',
      },
      {
        action: 'delete',
        shortcutKeys: ['del'],
        translationKey: 'operation.delete',
      },
    )

    return nextActions
  }, [canAccessSnippetsAndEvaluation, isAddToSnippetDisabled])

  const getActionLabel = useCallback((translationKey: string) => {
    if (translationKey === 'operation.delete')
      return t(translationKey, { ns: 'common', defaultValue: translationKey })

    return t(translationKey, { ns: 'workflow', defaultValue: translationKey })
  }, [t])

  const handleMenuAction = useCallback((action: ActionMenuItem['action']) => {
    switch (action) {
      case 'createSnippet':
        handleOpenCreateSnippetDialog()
        return
      case 'copy':
        handleSelectionContextmenuCancel()
        handleNodesCopy()
        return
      case 'duplicate':
        handleSelectionContextmenuCancel()
        handleNodesDuplicate()
        return
      case 'delete':
        handleSelectionContextmenuCancel()
        handleNodesDelete()
    }
  }, [handleNodesCopy, handleNodesDelete, handleNodesDuplicate, handleOpenCreateSnippetDialog, handleSelectionContextmenuCancel])

  const handleAlignNodes = useCallback((alignType: AlignTypeValue) => {
    if (getNodesReadOnly() || selectedNodes.length <= 1) {
      handleSelectionContextmenuCancel()
      return
    }

    workflowStore.setState({ nodeAnimation: false })

    const nodes = store.getState().getNodes()
    const nodesToAlign = getAlignableNodes(nodes, selectedNodes)

    if (nodesToAlign.length <= 1) {
      handleSelectionContextmenuCancel()
      return
    }

    const bounds = getAlignBounds(nodesToAlign)
    if (!bounds) {
      handleSelectionContextmenuCancel()
      return
    }

    if (alignType === AlignType.DistributeHorizontal || alignType === AlignType.DistributeVertical) {
      const distributedNodes = distributeNodes(nodesToAlign, nodes, alignType)
      if (distributedNodes) {
        store.getState().setNodes(distributedNodes)
        handleSelectionContextmenuCancel()

        const { setHelpLineHorizontal, setHelpLineVertical } = workflowStore.getState()
        setHelpLineHorizontal()
        setHelpLineVertical()

        handleSyncWorkflowDraft()
        saveStateToHistory(WorkflowHistoryEvent.NodeDragStop)
        return
      }
    }

    const newNodes = produce(nodes, (draft) => {
      const validNodesToAlign = nodesToAlign.filter(node => node.width && node.height)
      validNodesToAlign.forEach((nodeToAlign) => {
        const currentNode = draft.find(n => n.id === nodeToAlign.id)
        if (!currentNode)
          return

        alignNodePosition(currentNode, nodeToAlign, alignType, bounds)
      })
    })

    try {
      store.getState().setNodes(newNodes)
      handleSelectionContextmenuCancel()
      const { setHelpLineHorizontal, setHelpLineVertical } = workflowStore.getState()
      setHelpLineHorizontal()
      setHelpLineVertical()
      handleSyncWorkflowDraft()
      saveStateToHistory(WorkflowHistoryEvent.NodeDragStop)
    }
    catch (err) {
      console.error('Failed to update nodes:', err)
    }
  }, [store, workflowStore, selectedNodes, getNodesReadOnly, handleSyncWorkflowDraft, saveStateToHistory, handleSelectionContextmenuCancel])

  if ((!selectionMenu || !anchor) && !isCreateSnippetDialogOpen)
    return null

  return (
    <div data-testid="selection-contextmenu">
      <ContextMenu
        open
        onOpenChange={(open) => {
          if (!open)
            handleSelectionContextmenuCancel()
        }}
      >
        <ContextMenuContent
          positionerProps={anchor ? { anchor } : undefined}
          popupClassName="w-[240px] py-0"
        >
          <div className="p-1">
            {menuActions.map(item => (
              <ContextMenuItem
                key={item.action}
                data-testid={`selection-contextmenu-item-${item.action}`}
                disabled={item.disabled}
                className={cn(
                  'mx-0 h-8 justify-between gap-3 rounded-lg px-2 text-[14px] leading-5 font-normal text-text-secondary',
                  item.action === 'delete' && 'data-[highlighted]:bg-state-destructive-hover data-[highlighted]:text-text-destructive',
                )}
                onClick={() => handleMenuAction(item.action)}
              >
                <span>{getActionLabel(item.translationKey)}</span>
                {item.shortcutKeys && (
                  <ShortcutsName
                    keys={item.shortcutKeys}
                    textColor="secondary"
                  />
                )}
              </ContextMenuItem>
            ))}
          </div>
          <ContextMenuSeparator className="my-0" />
          <div className="p-1.5">
            <div className="flex items-center">
              {alignMenuItems.map((item) => {
                return (
                  <ContextMenuItem
                    key={item.alignType}
                    aria-label={t(item.translationKey, { defaultValue: item.translationKey, ns: 'workflow' })}
                    className="mx-0 h-8 w-8 justify-center rounded-md px-0 text-text-tertiary data-[highlighted]:bg-state-base-hover data-[highlighted]:text-text-secondary"
                    data-testid={`selection-contextmenu-item-${item.alignType}`}
                    onClick={() => handleAlignNodes(item.alignType)}
                  >
                    <span aria-hidden className={`${item.icon} h-4 w-4 ${item.iconClassName ?? ''}`.trim()} />
                  </ContextMenuItem>
                )
              })}
            </div>
          </div>
        </ContextMenuContent>
      </ContextMenu>
      {isCreateSnippetDialogOpen && (
        <CreateSnippetDialog
          isOpen={isCreateSnippetDialogOpen}
          selectedGraph={selectedGraphSnapshot}
          isSubmitting={isCreatingSnippet || createSnippetMutation.isPending}
          onClose={handleCloseCreateSnippetDialog}
          onConfirm={handleCreateSnippet}
        />
      )}
    </div>
  )
}

export default memo(SelectionContextmenu)
