import React, { createContext, useContext, useEffect, useRef, useState, useCallback, useMemo } from 'react'
import type { InventoryWindowState, PlayerState, ItemStack, SlotState } from '../types'
import type { InventoryConnector } from '../connector/types'
import { isItemEqual, getMaxStackSize } from '../utils/isItemEqual'
import {
  createInventoryDebugSession,
  getActionSlotIndexes,
  summarizeAction,
  summarizeItem,
  summarizeSlots,
  summarizeWindowState,
  type InventoryDebugSession,
} from '../debug/inventoryDebug'

export interface DragPreviewEntry {
  count: number
}

export interface InventoryContextValue {
  windowState: InventoryWindowState | null
  playerState: PlayerState | null
  heldItem: ItemStack | null
  setHeldItem: (item: ItemStack | null) => void
  connector: InventoryConnector | null
  sendAction: InventoryConnector['sendAction']
  isDragging: boolean
  dragSlots: number[]
  dragButton: 'left' | 'right' | null
  /** Client-side preview of item counts for each slot in the current drag */
  dragPreview: Map<number, DragPreviewEntry>
  /** Number of items remaining on the cursor during an active spread drag */
  dragRemainder: number | null
  startDrag: (slotIndex: number, button: 'left' | 'right') => void
  addDragSlot: (slotIndex: number) => void
  endDrag: () => void
  cancelDrag: () => void
  hoveredSlot: number | null
  setHoveredSlot: (slot: number | null) => void
  activeNumberKey: number | null
  setActiveNumberKey: (key: number | null) => void
  getSlot: (index: number) => SlotState | undefined
  /** Pixel offset from the cursor item's top-left to the grab point, preserving pick-up position */
  grabOffset: { x: number; y: number }
  setGrabOffset: (offset: { x: number; y: number }) => void
  /** Whether drag/spread operations are disabled */
  noDragSpread: boolean
  /** Whether P-key slot numbering mode is active */
  pKeyActive: boolean
  setPKeyActive: (v: boolean) => void
  /** Currently focused slot index (via P-key number entry) */
  focusedSlot: number | null
  setFocusedSlot: (slot: number | null) => void
  /** Pending first digit for P-key slot number entry */
  pKeyDigit: string
  setPKeyDigit: (d: string) => void
  /** Ref set to true when a drag just ended; cleared on next mouseDown.
   *  Used by Slot to suppress spurious click events that fire after endDrag. */
  dragEndedRef: React.MutableRefObject<boolean>
  /** When true, empty slot labels (Head, Chest, Legs, etc.) are not rendered */
  noPlaceholders: boolean
  /** Optional resolver for enchantment ID → display name (version-aware) */
  resolveEnchantmentName?: (id: number) => string | undefined
}

const InventoryContext = createContext<InventoryContextValue | null>(null)

export function useInventoryContext(): InventoryContextValue {
  const ctx = useContext(InventoryContext)
  if (!ctx) throw new Error('useInventoryContext must be used within InventoryProvider')
  return ctx
}

interface InventoryProviderProps {
  connector: InventoryConnector | null
  children: React.ReactNode
  noDragSpread?: boolean
  noPlaceholders?: boolean
  resolveEnchantmentName?: (id: number) => string | undefined
}

export function InventoryProvider({ connector, children, noDragSpread = false, noPlaceholders = false, resolveEnchantmentName }: InventoryProviderProps) {
  const [windowState, setWindowState] = useState<InventoryWindowState | null>(
    () => connector?.getWindowState() ?? null,
  )
  const [playerState, setPlayerState] = useState<PlayerState | null>(
    () => connector?.getPlayerState() ?? null,
  )
  const [heldItem, setHeldItemState] = useState<ItemStack | null>(
    () => connector?.getWindowState()?.heldItem ?? null,
  )
  const [hoveredSlot, setHoveredSlot] = useState<number | null>(null)
  const [isDragging, setIsDragging] = useState(false)
  const [dragSlots, setDragSlots] = useState<number[]>([])
  const [dragButton, setDragButton] = useState<'left' | 'right' | null>(null)
  const [dragPreview, setDragPreview] = useState<Map<number, DragPreviewEntry>>(new Map())
  const [dragRemainder, setDragRemainder] = useState<number | null>(null)
  const [activeNumberKey, setActiveNumberKey] = useState<number | null>(null)
  const [grabOffset, setGrabOffset] = useState<{ x: number; y: number }>({ x: 0, y: 0 })
  const [pKeyActive, setPKeyActive] = useState(false)
  const [focusedSlot, setFocusedSlot] = useState<number | null>(null)
  const [pKeyDigit, setPKeyDigit] = useState('')

  const connectorRef = useRef(connector)
  connectorRef.current = connector

  // Refs so endDrag can read current values without being in its dep array
  const heldItemRef = useRef(heldItem)
  heldItemRef.current = heldItem
  const windowStateRef = useRef(windowState)
  windowStateRef.current = windowState
  const dragButtonRef = useRef(dragButton)
  dragButtonRef.current = dragButton

  // Set to true when endDrag fires; cleared on the next mouseDown in Slot.
  // Prevents spurious mouseUp events from sending unwanted clicks after a drag.
  const dragEndedRef = useRef(false)
  /** Latest full context value; used by the DevTools debug API (`globalThis.__mcInv.state`). */
  const valueRef = useRef<InventoryContextValue | null>(null)
  const debugSessionRef = useRef<InventoryDebugSession | null>(null)

  useEffect(() => {
    const session = createInventoryDebugSession('inventory-provider', () => {
      const value = valueRef.current
      if (!value) return null
      return {
        windowState: value.windowState,
        playerState: value.playerState,
        heldItem: value.heldItem,
        isDragging: value.isDragging,
        dragSlots: [...value.dragSlots],
      }
    })
    debugSessionRef.current = session
    session.log({ event: 'provider.mount' })
    return () => {
      session.log({ event: 'provider.unmount' })
      session.dispose()
      if (debugSessionRef.current === session) debugSessionRef.current = null
    }
  }, [])

  useEffect(() => {
    if (!connector) return
    setWindowState(connector.getWindowState())
    setPlayerState(connector.getPlayerState())

    return connector.subscribe((event) => {
      if (event.type === 'windowOpen' || event.type === 'windowUpdate') {
        debugSessionRef.current?.log({
          event: `connector.${event.type}`,
          windowId: event.state.windowId,
          windowType: event.state.type,
          slots: summarizeSlots(event.state.slots),
          heldItem: summarizeItem(event.state.heldItem),
        })
        setWindowState({ ...event.state })
        setHeldItemState(event.state.heldItem)
      } else if (event.type === 'windowClose') {
        debugSessionRef.current?.log({ event: 'connector.windowClose' })
        setWindowState(null)
        setHeldItemState(null)
        setIsDragging(false)
        setDragSlots([])
      } else if (event.type === 'playerUpdate') {
        debugSessionRef.current?.log({
          event: 'connector.playerUpdate',
          slots: summarizeSlots(event.state.inventory),
          data: {
            activeHotbarSlot: event.state.activeHotbarSlot,
          },
        })
        setPlayerState(event.state)
      } else if (event.type === 'heldItemChange') {
        debugSessionRef.current?.log({
          event: 'connector.heldItemChange',
          heldItem: summarizeItem(event.item),
        })
        setHeldItemState(event.item)
      }
    })
  }, [connector])

  const setHeldItem = useCallback((item: ItemStack | null) => {
    setHeldItemState(item)
  }, [])

  const sendAction = useCallback<InventoryConnector['sendAction']>(
    (action) => {
      const slotIndexes = getActionSlotIndexes(action)
      const state = valueRef.current?.windowState ?? connectorRef.current?.getWindowState() ?? null
      debugSessionRef.current?.log({
        event: 'ui.action',
        windowId: state?.windowId,
        windowType: state?.type,
        action: summarizeAction(action),
        slots: state ? summarizeSlots(state.slots, slotIndexes) : undefined,
        heldItem: summarizeItem(valueRef.current?.heldItem ?? state?.heldItem),
        data: {
          before: summarizeWindowState(state, slotIndexes),
        },
      })
      return connectorRef.current?.sendAction(action)
    },
    [],
  )

  const computeDragPreview = useCallback((slots: number[], button: 'left' | 'right', held: ItemStack | null, ws: InventoryWindowState | null): { preview: Map<number, DragPreviewEntry>; remainder: number } => {
    const preview = new Map<number, DragPreviewEntry>()
    if (!held || slots.length === 0) return { preview, remainder: held?.count ?? 0 }

    const maxStack = getMaxStackSize(held)
    let totalPlaced = 0

    if (button === 'left') {
      // Only spread into compatible slots (empty or same item type)
      const compatibleSlots = slots.filter((idx) => {
        const existingItem = ws?.slots.find((s) => s.index === idx)?.item
        return !existingItem || isItemEqual(existingItem, held)
      })
      if (compatibleSlots.length === 0) return { preview, remainder: held.count }
      const perSlot = Math.floor(held.count / compatibleSlots.length)
      // Vanilla behavior: if perSlot=0 (more slots than items), nothing is distributed
      if (perSlot === 0) return { preview, remainder: held.count }
      for (const idx of compatibleSlots) {
        const existingCount = ws?.slots.find((s) => s.index === idx)?.item?.count ?? 0
        const total = Math.min(existingCount + perSlot, maxStack)
        const added = total - existingCount
        totalPlaced += added
        preview.set(idx, { count: total })
      }
    } else {
      for (const idx of slots) {
        const existing = ws?.slots.find((s) => s.index === idx)
        const existingItem = existing?.item
        if (existingItem && !isItemEqual(existingItem, held)) continue
        const existingCount = existingItem ? existingItem.count : 0
        const total = Math.min(existingCount + 1, maxStack)
        const added = total - existingCount
        totalPlaced += added
        preview.set(idx, { count: total })
      }
    }
    return { preview, remainder: held.count - totalPlaced }
  }, [])

  const startDrag = useCallback((slotIndex: number, button: 'left' | 'right') => {
    if (noDragSpread) return
    dragEndedRef.current = false
    setIsDragging(true)
    setDragButton(button)
    setDragSlots([slotIndex])
    setDragPreview(new Map())
    setDragRemainder(null)
  }, [noDragSpread])

  const addDragSlot = useCallback((slotIndex: number) => {
    setDragSlots((prev) => {
      if (prev.includes(slotIndex)) return prev
      const next = [...prev, slotIndex]
      const result = computeDragPreview(next, dragButton!, heldItem, windowState)
      setDragPreview(result.preview)
      setDragRemainder(result.remainder)
      return next
    })
  }, [computeDragPreview, dragButton, heldItem, windowState])

  const endDrag = useCallback(() => {
    dragEndedRef.current = true
    setDragSlots((slots) => {
      const button = dragButtonRef.current
      const held = heldItemRef.current
      const ws = windowStateRef.current

      // Only send drag action if multiple slots were involved (single slot = normal click)
      if (slots.length > 1 && button && held) {
        debugSessionRef.current?.log({
          event: 'ui.drag.optimisticUpdate',
          windowId: ws?.windowId,
          windowType: ws?.type,
          action: summarizeAction({ type: 'drag', slots, button }),
          slots: ws ? summarizeSlots(ws.slots, slots) : undefined,
          heldItem: summarizeItem(held),
          data: {
            before: summarizeWindowState(ws, slots),
          },
        })
        connectorRef.current?.sendAction({ type: 'drag', slots, button })

        // Optimistic client-side update: apply item distribution immediately
        // so slots visually update before server responds.
        const maxStack = getMaxStackSize(held)
        const newSlots = ws ? [...ws.slots] : []

        if (button === 'left') {
          const compatibleSlots = slots.filter((idx) => {
            const existing = newSlots.find((s) => s.index === idx)?.item
            return !existing || isItemEqual(existing, held)
          })
          if (compatibleSlots.length > 0) {
            const perSlot = Math.floor(held.count / compatibleSlots.length)
            if (perSlot > 0) {
              let totalPlaced = 0
              for (const idx of compatibleSlots) {
                const existingIdx = newSlots.findIndex((s) => s.index === idx)
                const existingCount = existingIdx >= 0 ? (newSlots[existingIdx].item?.count ?? 0) : 0
                const add = Math.min(perSlot, maxStack - existingCount)
                totalPlaced += add
                const newCount = existingCount + add
                if (existingIdx >= 0) {
                  newSlots[existingIdx] = { index: idx, item: { ...held, count: newCount } }
                } else {
                  newSlots.push({ index: idx, item: { ...held, count: newCount } })
                }
              }
              if (ws) setWindowState({ ...ws, slots: newSlots })
              const remaining = held.count - totalPlaced
              if (remaining > 0) setHeldItemState({ ...held, count: remaining })
              else setHeldItemState(null)
            }
          }
        } else {
          // Right-click drag: place 1 per slot
          let remaining = held.count
          for (const idx of slots) {
            if (remaining <= 0) break
            const existingIdx = newSlots.findIndex((s) => s.index === idx)
            const existingItem = existingIdx >= 0 ? newSlots[existingIdx].item : null
            if (existingItem && !isItemEqual(existingItem, held)) continue
            const existingCount = existingItem?.count ?? 0
            const newCount = Math.min(existingCount + 1, maxStack)
            if (existingIdx >= 0) {
              newSlots[existingIdx] = { index: idx, item: { ...held, count: newCount } }
            } else {
              newSlots.push({ index: idx, item: { ...held, count: newCount } })
            }
            remaining--
          }
          if (ws) setWindowState({ ...ws, slots: newSlots })
          if (remaining <= 0) setHeldItemState(null)
          else setHeldItemState({ ...held, count: remaining })
        }
      }
      return []
    })
    setIsDragging(false)
    setDragButton(null)
    setDragPreview(new Map())
    setDragRemainder(null)
  }, [])

  const cancelDrag = useCallback(() => {
    setIsDragging(false)
    setDragSlots([])
    setDragButton(null)
    setDragPreview(new Map())
    setDragRemainder(null)
  }, [])

  // Global mouseup listener to finalize drag when the button is released outside slot elements.
  // Slot.handleMouseUp calls e.stopPropagation(), so when mouseup lands on a slot the window
  // listener does NOT fire — the two handlers are mutually exclusive.
  useEffect(() => {
    if (!isDragging) return

    const handleGlobalMouseUp = () => {
      endDrag()
    }
    const handleBlur = () => {
      cancelDrag()
    }

    window.addEventListener('mouseup', handleGlobalMouseUp)
    window.addEventListener('blur', handleBlur)
    return () => {
      window.removeEventListener('mouseup', handleGlobalMouseUp)
      window.removeEventListener('blur', handleBlur)
    }
  }, [isDragging, endDrag, cancelDrag])

  const getSlot = useCallback(
    (index: number) => {
      return windowState?.slots.find((s) => s.index === index)
    },
    [windowState],
  )

  const value: InventoryContextValue = {
    windowState,
    playerState,
    heldItem,
    setHeldItem,
    connector,
    sendAction,
    isDragging,
    dragSlots,
    dragButton,
    dragPreview,
    dragRemainder,
    startDrag,
    addDragSlot,
    endDrag,
    cancelDrag,
    hoveredSlot,
    setHoveredSlot,
    activeNumberKey,
    setActiveNumberKey,
    getSlot,
    grabOffset,
    setGrabOffset,
    noDragSpread,
    noPlaceholders,
    pKeyActive,
    setPKeyActive,
    focusedSlot,
    setFocusedSlot,
    pKeyDigit,
    setPKeyDigit,
    dragEndedRef,
    resolveEnchantmentName,
  }

  valueRef.current = value

  return <InventoryContext.Provider value={value}>{children}</InventoryContext.Provider>
}
