import { useCallback, useEffect, useRef, useState } from 'react'
import type { MenuItemNode, MenuRow, MenuTarget, MenuTree } from './client.js'
import { useMenuClient } from './provider.js'

function toError(e: unknown): Error {
  return e instanceof Error ? e : new Error(String(e))
}

function removeNodeFromTree(items: MenuItemNode[], itemId: string): MenuItemNode[] {
  return items
    .filter((item) => item.id !== itemId)
    .map((item) => ({ ...item, children: removeNodeFromTree(item.children, itemId) }))
}

function findNodeInTree(items: MenuItemNode[], itemId: string): MenuItemNode | null {
  for (const item of items) {
    if (item.id === itemId) return item
    const found = findNodeInTree(item.children, itemId)
    if (found) return found
  }
  return null
}

function updateSubtreeDepth(node: MenuItemNode, parentDepth: number): MenuItemNode {
  const depth = parentDepth + 1
  return {
    ...node,
    depth,
    children: node.children.map((child, index) => ({
      ...updateSubtreeDepth(child, depth),
      position: index,
    })),
  }
}

function applyReorderToTree(
  items: MenuItemNode[],
  parentId: string | null,
  orderedIds: string[],
): MenuItemNode[] {
  if (parentId === null) {
    const byId = new Map(items.map((item) => [item.id, item]))
    return orderedIds.map((id, position) => {
      const node = byId.get(id)!
      return { ...node, position, parentId: null, depth: 0 }
    })
  }
  return items.map((item) => {
    if (item.id === parentId) {
      const byId = new Map(item.children.map((child) => [child.id, child]))
      const children = orderedIds.map((id, position) => {
        const node = byId.get(id)!
        return { ...node, position, parentId, depth: item.depth + 1 }
      })
      return { ...item, children }
    }
    return { ...item, children: applyReorderToTree(item.children, parentId, orderedIds) }
  })
}

function applyMoveToTree(
  items: MenuItemNode[],
  itemId: string,
  newParentId: string | null,
  newPosition: number,
): MenuItemNode[] {
  const node = findNodeInTree(items, itemId)
  if (!node) return items

  const without = removeNodeFromTree(items, itemId)

  if (newParentId === null) {
    const roots = [...without]
    const moved = {
      ...node,
      parentId: null,
      depth: 0,
      children: node.children.map((child, index) => ({
        ...updateSubtreeDepth(child, 0),
        position: index,
      })),
    }
    roots.splice(newPosition, 0, moved)
    return roots.map((item, position) => ({ ...item, position }))
  }

  return insertNodeUnderParent(without, newParentId, newPosition, node)
}

function insertNodeUnderParent(
  items: MenuItemNode[],
  parentId: string,
  position: number,
  node: MenuItemNode,
): MenuItemNode[] {
  return items.map((item) => {
    if (item.id === parentId) {
      const parentDepth = item.depth
      const moved = {
        ...node,
        parentId,
        depth: parentDepth + 1,
        children: node.children.map((child, index) => ({
          ...updateSubtreeDepth(child, parentDepth + 1),
          position: index,
        })),
      }
      const children = [...item.children]
      children.splice(position, 0, moved)
      return {
        ...item,
        children: children.map((child, index) => ({ ...child, position: index })),
      }
    }
    return {
      ...item,
      children: insertNodeUnderParent(item.children, parentId, position, node),
    }
  })
}

export function useMenus(opts: { initialData?: MenuRow[] } = {}) {
  const client = useMenuClient()
  const hasSeed = opts.initialData !== undefined
  const [menus, setMenus] = useState<MenuRow[]>(opts.initialData ?? [])
  const [loading, setLoading] = useState(!hasSeed)
  const [error, setError] = useState<Error | null>(null)
  const [reloadToken, setReloadToken] = useState(0)

  const clientRef = useRef(client)
  clientRef.current = client

  const reload = useCallback(() => {
    setReloadToken((t) => t + 1)
  }, [])

  useEffect(() => {
    if (hasSeed && reloadToken === 0) return

    let cancelled = false
    setLoading(true)
    setError(null)

    void clientRef.current
      .listMenus()
      .then((next) => {
        if (!cancelled) {
          setMenus(next)
          setLoading(false)
        }
      })
      .catch((e: unknown) => {
        if (!cancelled) {
          setError(toError(e))
          setLoading(false)
        }
      })

    return () => {
      cancelled = true
    }
  }, [hasSeed, reloadToken])

  const deleteMenu = useCallback(
    async (menuId: string) => {
      const snapshot = menus

      setError(null)
      setMenus((prev) => prev.filter((menu) => menu.id !== menuId))

      try {
        await clientRef.current.deleteMenu(menuId)
      } catch (e: unknown) {
        setMenus(snapshot)
        const err = toError(e)
        setError(err)
        throw err
      }
    },
    [menus],
  )

  const createMenu = useCallback(async (input: { key: string; label: string }) => {
    const row = await clientRef.current.createMenu(input)
    setMenus((prev) => [...prev, row])
    return row
  }, [])

  const updateMenu = useCallback(async (menuId: string, patch: { label?: string }) => {
    const row = await clientRef.current.updateMenu(menuId, patch)
    setMenus((prev) => prev.map((menu) => (menu.id === menuId ? row : menu)))
    return row
  }, [])

  return { menus, loading, error, reload, createMenu, updateMenu, deleteMenu }
}

export function useMenuTree(key: string, opts: { initialData?: MenuTree | null } = {}) {
  const client = useMenuClient()
  const hasSeed = opts.initialData !== undefined
  const [tree, setTree] = useState<MenuTree | null>(opts.initialData ?? null)
  const [loading, setLoading] = useState(!hasSeed)
  const [error, setError] = useState<Error | null>(null)
  const [reloadToken, setReloadToken] = useState(0)

  const clientRef = useRef(client)
  clientRef.current = client

  const menuKey = key
  const prevMenuKey = useRef(menuKey)

  const reload = useCallback(() => {
    setReloadToken((t) => t + 1)
  }, [])

  useEffect(() => {
    const keyChanged = prevMenuKey.current !== menuKey
    prevMenuKey.current = menuKey

    if (hasSeed && reloadToken === 0 && !keyChanged) return

    let cancelled = false
    setLoading(true)
    setError(null)

    void clientRef.current
      .getMenuTree(key)
      .then((next) => {
        if (!cancelled) {
          setTree(next)
          setLoading(false)
        }
      })
      .catch((e: unknown) => {
        if (!cancelled) {
          setError(toError(e))
          setLoading(false)
        }
      })

    return () => {
      cancelled = true
    }
  }, [menuKey, hasSeed, reloadToken])

  const removeItem = useCallback(
    async (itemId: string) => {
      const snapshot = tree

      setError(null)
      setTree((prev) =>
        prev ? { ...prev, items: removeNodeFromTree(prev.items, itemId) } : prev,
      )

      try {
        await clientRef.current.removeItem(itemId)
      } catch (e: unknown) {
        setTree(snapshot)
        const err = toError(e)
        setError(err)
        throw err
      }
    },
    [tree],
  )

  const addItem = useCallback(
    async (
      menuId: string,
      input: { label: string; target: MenuTarget; parentId?: string | null },
    ) => {
      const row = await clientRef.current.addItem(menuId, input)
      reload()
      return row
    },
    [reload],
  )

  const updateItem = useCallback(
    async (
      itemId: string,
      patch: { label?: string; target?: MenuTarget; openInNew?: boolean },
    ) => {
      const row = await clientRef.current.updateItem(itemId, patch)
      reload()
      return row
    },
    [reload],
  )

  const moveItem = useCallback(
    async (itemId: string, input: { newParentId: string | null; newPosition: number }) => {
      const snapshot = tree

      setError(null)
      setTree((prev) =>
        prev
          ? {
              ...prev,
              items: applyMoveToTree(prev.items, itemId, input.newParentId, input.newPosition),
            }
          : prev,
      )

      try {
        return await clientRef.current.moveItem(itemId, input)
      } catch (e: unknown) {
        setTree(snapshot)
        const err = toError(e)
        setError(err)
        throw err
      }
    },
    [tree],
  )

  const reorder = useCallback(
    async (menuId: string, parentId: string | null, orderedIds: string[]) => {
      const snapshot = tree

      setError(null)
      setTree((prev) =>
        prev
          ? { ...prev, items: applyReorderToTree(prev.items, parentId, orderedIds) }
          : prev,
      )

      try {
        await clientRef.current.reorder(menuId, parentId, orderedIds)
      } catch (e: unknown) {
        setTree(snapshot)
        const err = toError(e)
        setError(err)
        throw err
      }
    },
    [tree],
  )

  return { tree, loading, error, reload, addItem, updateItem, removeItem, moveItem, reorder }
}
