import type { ChildrenList, PartialElement } from '@furystack/shades'
import { createComponent, Shade } from '@furystack/shades'
import { ObservableValue } from '@furystack/utils'
import { cssVariableTheme } from '../../services/css-variable-theme.js'
import type { TreeService } from '../../services/tree-service.js'
import { TreeItem } from './tree-item.js'

let nextTreeId = 0

export type TreeItemState = {
  isFocused: boolean
  isSelected: boolean
  level: number
  hasChildren: boolean
  isExpanded: boolean
}

export type TreeProps<T> = {
  rootItems: T[]
  treeService: TreeService<T>
  renderItem: (item: T, state: TreeItemState) => JSX.Element
  renderIcon?: (item: T, isExpanded: boolean) => JSX.Element
  variant?: 'contained' | 'outlined'
  onItemActivate?: (item: T) => void
  onSelectionChange?: (selected: T[]) => void
  /**
   * Section name for spatial navigation scoping.
   * Sets `data-nav-section` on the tree host so that SpatialNavigationService
   * constrains arrow-key navigation within the tree.
   * Auto-generated per instance when not provided.
   */
  navSection?: string
} & PartialElement<HTMLDivElement>

export const Tree: <T>(props: TreeProps<T>, children: ChildrenList) => JSX.Element<any> = Shade({
  customElementName: 'shade-tree',
  css: {
    display: 'block',
    fontFamily: cssVariableTheme.typography.fontFamily,
    width: '100%',
    overflow: 'auto',
  },
  render: ({ props, useDisposable, useObservable, useHostProps, useState }) => {
    const [navSectionId] = useState('navSectionId', String(nextTreeId++))

    useDisposable('keydown-handler', () => {
      const listener = (ev: KeyboardEvent) => {
        props.treeService.handleKeyDown(ev)

        if (ev.key === 'Enter' && props.treeService.hasFocus.getValue()) {
          const focusedItem = props.treeService.focusedItem.getValue()
          if (focusedItem && props.onItemActivate) {
            props.onItemActivate(focusedItem)
          }
        }
      }
      window.addEventListener('keydown', listener, true)
      return { [Symbol.dispose]: () => window.removeEventListener('keydown', listener, true) }
    })

    if (props.treeService.rootItems.getValue() !== props.rootItems) {
      props.treeService.rootItems.setValue(props.rootItems)
      props.treeService.updateFlattenedNodes()
    }

    const treeInstanceId = useDisposable('treeInstanceId', () => ({
      value: Math.random().toString(36).slice(2),
      [Symbol.dispose]() {},
    }))

    useDisposable('clickAway', () => {
      const listener = (ev: MouseEvent) => {
        const isInside = ev
          .composedPath()
          .some((el) => el instanceof HTMLElement && el.dataset.treeInstanceId === treeInstanceId.value)
        if (!isInside) {
          props.treeService.hasFocus.setValue(false)
        }
      }
      window.addEventListener('click', listener, true)
      return { [Symbol.dispose]: () => window.removeEventListener('click', listener, true) }
    })

    if (props.onSelectionChange) {
      const { onSelectionChange } = props
      useDisposable('selectionChangeCallback', () =>
        props.treeService.selection.subscribe((newSelection) => {
          onSelectionChange(newSelection)
        }),
      )
    }

    useHostProps({
      'data-variant': props.variant || undefined,
      'data-tree-instance-id': treeInstanceId.value,
      'data-nav-section': props.navSection ?? `tree-${navSectionId}`,
      role: 'tree',
      'aria-multiselectable': 'true',
      onclick: () => props.treeService.hasFocus.setValue(true),
      onfocusout: (ev: FocusEvent) => {
        const hostEl = ev.currentTarget as HTMLElement
        if (!ev.relatedTarget || !hostEl.contains(ev.relatedTarget as Node)) {
          props.treeService.hasFocus.setValue(false)
        }
      },
    })

    const [flattenedNodes] = useObservable('flattenedNodes', props.treeService.flattenedNodes)

    // eslint-disable-next-line furystack/require-use-observable-for-render -- Used as persistent ref, not reactive state; read and written synchronously in same render cycle
    const previousItemsRef = useDisposable('previousTreeItems', () => new ObservableValue<Set<unknown>>(new Set()))
    const previousItems = previousItemsRef.getValue()
    const currentItems = new Set<unknown>(flattenedNodes.map((n) => n.item))
    previousItemsRef.setValue(currentItems)

    return (
      <>
        {flattenedNodes.map((nodeInfo) => (
          <TreeItem
            item={nodeInfo.item}
            treeService={props.treeService}
            nodeInfo={nodeInfo}
            isNew={!previousItems.has(nodeInfo.item)}
            renderItem={props.renderItem}
            renderIcon={props.renderIcon}
            onActivate={props.onItemActivate}
          />
        ))}
      </>
    )
  },
})
