import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { cn } from '@/utils/cn'

export interface DropdownMenuProps {
  trigger: React.ReactNode
  children: React.ReactNode
  open?: boolean
  defaultOpen?: boolean
  onOpenChange?: (open: boolean) => void
  align?: 'start' | 'center' | 'end'
  side?: 'top' | 'right' | 'bottom' | 'left'
  width?: 'trigger' | 'sm' | 'md' | 'lg'
  className?: string
}

export interface DropdownMenuItemProps {
  leftIcon?: string
  rightIcon?: string
  rightSlot?: React.ReactNode
  description?: string
  disabled?: boolean
  tone?: 'default' | 'danger'
  onSelect?: () => void
  children: React.ReactNode
}

interface DropdownMenuContextValue {
  closeMenu: (restoreFocus?: boolean) => void
}

const DropdownMenuContext = React.createContext<DropdownMenuContextValue | null>(null)

const widthStyles: Record<Exclude<DropdownMenuProps['width'], undefined | 'trigger'>, string> = {
  sm: 'w-48',
  md: 'w-60',
  lg: 'w-80',
}

type MenuPosition = {
  top: number
  left: number
  minWidth?: number
}

type TriggerElementProps = React.HTMLAttributes<HTMLElement> & React.RefAttributes<HTMLElement>

function assignRef<T>(ref: React.Ref<T> | undefined, value: T | null) {
  if (!ref) return
  if (typeof ref === 'function') {
    ref(value)
    return
  }
  ;(ref as React.MutableRefObject<T | null>).current = value
}

function composeRefs<T>(...refs: Array<React.Ref<T> | undefined>) {
  return (value: T | null) => {
    refs.forEach((ref) => assignRef(ref, value))
  }
}

function getEnabledItems(menu: HTMLElement | null): HTMLElement[] {
  if (!menu) return []
  return Array.from(
    menu.querySelectorAll<HTMLElement>('[data-cp-dropdown-item="true"]:not([aria-disabled="true"])')
  )
}

function clamp(value: number, min: number, max: number): number {
  return Math.min(Math.max(value, min), max)
}

const DropdownMenuRoot: React.FC<DropdownMenuProps> = ({
  trigger,
  children,
  open,
  defaultOpen = false,
  onOpenChange,
  align = 'start',
  side = 'bottom',
  width = 'sm',
  className,
}) => {
  const isControlled = open !== undefined
  const [internalOpen, setInternalOpen] = useState(defaultOpen)
  const isOpen = isControlled ? Boolean(open) : internalOpen
  const triggerRef = useRef<HTMLElement>(null)
  const menuRef = useRef<HTMLDivElement>(null)
  const focusOnOpenRef = useRef<'first' | 'last' | null>(null)
  const [position, setPosition] = useState<MenuPosition>({ top: 0, left: 0 })

  const setOpen = useCallback(
    (nextOpen: boolean) => {
      if (!isControlled) setInternalOpen(nextOpen)
      onOpenChange?.(nextOpen)
    },
    [isControlled, onOpenChange]
  )

  const closeMenu = useCallback(
    (restoreFocus = true) => {
      setOpen(false)
      if (restoreFocus) triggerRef.current?.focus()
    },
    [setOpen]
  )

  const updatePosition = useCallback(() => {
    const triggerEl = triggerRef.current
    const menuEl = menuRef.current
    if (!triggerEl || !menuEl || typeof window === 'undefined') return

    const margin = 8
    const gap = 6
    const rect = triggerEl.getBoundingClientRect()
    const menuWidth = width === 'trigger' ? rect.width : menuEl.offsetWidth
    const menuHeight = menuEl.offsetHeight

    let top = rect.bottom + gap
    let left = rect.left

    if (side === 'top') {
      top = rect.top - menuHeight - gap
    } else if (side === 'right') {
      top = rect.top + rect.height / 2 - menuHeight / 2
      left = rect.right + gap
    } else if (side === 'left') {
      top = rect.top + rect.height / 2 - menuHeight / 2
      left = rect.left - menuWidth - gap
    }

    if (side === 'top' || side === 'bottom') {
      if (align === 'center') left = rect.left + rect.width / 2 - menuWidth / 2
      if (align === 'end') left = rect.right - menuWidth
    } else {
      if (align === 'center') top = rect.top + rect.height / 2 - menuHeight / 2
      if (align === 'end') top = rect.bottom - menuHeight
    }

    setPosition({
      top: clamp(top, margin, window.innerHeight - menuHeight - margin),
      left: clamp(left, margin, window.innerWidth - menuWidth - margin),
      minWidth: width === 'trigger' ? rect.width : undefined,
    })
  }, [align, side, width])

  const focusMenuItem = useCallback((target: 'first' | 'last' = 'first') => {
    const enabledItems = getEnabledItems(menuRef.current)
    const item = target === 'last' ? enabledItems[enabledItems.length - 1] : enabledItems[0]
    item?.focus()
  }, [])

  useLayoutEffect(() => {
    if (!isOpen) return
    updatePosition()
  }, [isOpen, updatePosition, children])

  useLayoutEffect(() => {
    if (!isOpen || !focusOnOpenRef.current) return
    const target = focusOnOpenRef.current
    focusOnOpenRef.current = null
    focusMenuItem(target)
  }, [focusMenuItem, isOpen, children])

  useEffect(() => {
    if (!isOpen || typeof document === 'undefined') return

    const handlePointerDown = (event: MouseEvent) => {
      const target = event.target as Node
      if (triggerRef.current?.contains(target) || menuRef.current?.contains(target)) return
      closeMenu(false)
    }

    const handleKeyDown = (event: KeyboardEvent) => {
      if (event.key === 'Escape') {
        event.preventDefault()
        closeMenu(true)
      }
    }

    document.addEventListener('mousedown', handlePointerDown)
    document.addEventListener('keydown', handleKeyDown)
    window.addEventListener('resize', updatePosition)
    window.addEventListener('scroll', updatePosition, true)

    return () => {
      document.removeEventListener('mousedown', handlePointerDown)
      document.removeEventListener('keydown', handleKeyDown)
      window.removeEventListener('resize', updatePosition)
      window.removeEventListener('scroll', updatePosition, true)
    }
  }, [closeMenu, isOpen, updatePosition])

  const handleMenuKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
    const enabledItems = getEnabledItems(menuRef.current)
    if (enabledItems.length === 0) return

    const activeIndex = enabledItems.indexOf(document.activeElement as HTMLElement)

    if (event.key === 'ArrowDown') {
      event.preventDefault()
      enabledItems[(activeIndex + 1) % enabledItems.length]?.focus()
    } else if (event.key === 'ArrowUp') {
      event.preventDefault()
      enabledItems[(activeIndex - 1 + enabledItems.length) % enabledItems.length]?.focus()
    } else if (event.key === 'Home') {
      event.preventDefault()
      enabledItems[0]?.focus()
    } else if (event.key === 'End') {
      event.preventDefault()
      enabledItems[enabledItems.length - 1]?.focus()
    } else if (event.key === 'Enter' || event.key === ' ') {
      if (document.activeElement instanceof HTMLElement && enabledItems.includes(document.activeElement)) {
        event.preventDefault()
        document.activeElement.click()
      }
    }
  }

  const handleTriggerKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {
    if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return
    event.preventDefault()
    const target = event.key === 'ArrowUp' ? 'last' : 'first'
    if (isOpen) {
      focusMenuItem(target)
      return
    }
    focusOnOpenRef.current = target
    setOpen(true)
  }

  const contextValue = useMemo<DropdownMenuContextValue>(() => ({ closeMenu }), [closeMenu])

  const triggerElement = React.isValidElement(trigger)
    ? (() => {
        const element = trigger as React.ReactElement<TriggerElementProps> & { ref?: React.Ref<HTMLElement> }
        return React.cloneElement<TriggerElementProps>(element, {
          ref: composeRefs(element.ref, triggerRef),
          'aria-haspopup': 'menu',
          'aria-expanded': isOpen,
          onClick: (event: React.MouseEvent<HTMLElement>) => {
            element.props.onClick?.(event)
            if (!event.defaultPrevented) setOpen(!isOpen)
          },
          onKeyDown: (event: React.KeyboardEvent<HTMLElement>) => {
            element.props.onKeyDown?.(event)
            if (!event.defaultPrevented) handleTriggerKeyDown(event)
          },
        })
      })()
    : (
        <button
          ref={triggerRef as React.RefObject<HTMLButtonElement>}
          type="button"
          aria-haspopup="menu"
          aria-expanded={isOpen}
          onClick={() => setOpen(!isOpen)}
          onKeyDown={handleTriggerKeyDown}
        >
          {trigger}
        </button>
      )

  const menu = isOpen ? (
    <DropdownMenuContext.Provider value={contextValue}>
      <div
        ref={menuRef}
        role="menu"
        tabIndex={-1}
        onKeyDown={handleMenuKeyDown}
        className={cn(
          'fixed z-40 rounded-[10px] border border-slate-200/80 bg-white p-1.5 shadow-soft-lg',
          'outline-none',
          width !== 'trigger' && widthStyles[width],
          className
        )}
        style={{
          top: position.top,
          left: position.left,
          minWidth: position.minWidth,
        }}
      >
        {children}
      </div>
    </DropdownMenuContext.Provider>
  ) : null

  return (
    <>
      {triggerElement}
      {typeof document !== 'undefined' && menu ? createPortal(menu, document.body) : menu}
    </>
  )
}

const DropdownMenuItem: React.FC<DropdownMenuItemProps> = ({
  leftIcon,
  rightIcon,
  rightSlot,
  description,
  disabled = false,
  tone = 'default',
  onSelect,
  children,
}) => {
  const context = React.useContext(DropdownMenuContext)

  const handleClick = () => {
    if (disabled) return
    onSelect?.()
    context?.closeMenu(true)
  }

  return (
    <button
      type="button"
      role="menuitem"
      tabIndex={disabled ? -1 : -1}
      aria-disabled={disabled ? 'true' : undefined}
      data-cp-dropdown-item="true"
      onClick={handleClick}
      className={cn(
        'flex w-full items-center gap-2 rounded-lg px-2.5 py-2 text-left text-[13px] transition-colors',
        'focus:outline-2 focus:outline-teal-400 focus:outline-offset-1',
        disabled && 'cursor-not-allowed opacity-45',
        !disabled && tone === 'default' && 'text-slate-700 hover:bg-slate-50 focus:bg-slate-50',
        !disabled && tone === 'danger' && 'text-red-600 hover:bg-red-50 focus:bg-red-50'
      )}
    >
      {leftIcon && (
        <span className="material-symbols-outlined text-[18px] leading-none text-current" aria-hidden="true">
          {leftIcon}
        </span>
      )}
      <span className="min-w-0 flex-1">
        <span className="block truncate font-semibold">{children}</span>
        {description && <span className="block truncate text-[11px] font-medium text-slate-400">{description}</span>}
      </span>
      {rightSlot ?? (
        rightIcon ? (
          <span className="material-symbols-outlined text-[18px] leading-none text-current" aria-hidden="true">
            {rightIcon}
          </span>
        ) : null
      )}
    </button>
  )
}

const DropdownMenuLabel: React.FC<{ children: React.ReactNode; className?: string }> = ({
  children,
  className,
}) => (
  <div className={cn('px-2.5 py-1.5 text-[11px] font-extrabold uppercase text-slate-400', className)}>
    {children}
  </div>
)

const DropdownMenuSeparator: React.FC<{ className?: string }> = ({ className }) => (
  <div role="separator" className={cn('my-1 h-px bg-slate-100', className)} />
)

const DropdownMenuGroup: React.FC<{ children: React.ReactNode; className?: string }> = ({
  children,
  className,
}) => (
  <div role="group" className={className}>
    {children}
  </div>
)

export const DropdownMenu = Object.assign(DropdownMenuRoot, {
  Item: DropdownMenuItem,
  Label: DropdownMenuLabel,
  Separator: DropdownMenuSeparator,
  Group: DropdownMenuGroup,
})
