import { ObservableValue } from '@furystack/utils'

export type ListServiceOptions<T> = {
  /**
   * An optional field that can be used for type-ahead search
   */
  searchField?: keyof T
}

/**
 * Service for managing list state including focus, selection, and keyboard navigation
 */
export class ListService<T> implements Disposable {
  public [Symbol.dispose]() {
    this.items[Symbol.dispose]()
    this.selection[Symbol.dispose]()
    this.searchTerm[Symbol.dispose]()
    this.hasFocus[Symbol.dispose]()
    this.focusedItem[Symbol.dispose]()
  }

  public isSelected = (item: T) => this.selection.getValue().includes(item)

  public addToSelection = (item: T) => {
    this.selection.setValue([...this.selection.getValue(), item])
  }

  public removeFromSelection = (item: T) => {
    this.selection.setValue(this.selection.getValue().filter((e) => e !== item))
  }

  public toggleSelection = (item: T) => {
    if (this.isSelected(item)) {
      this.removeFromSelection(item)
    } else {
      this.addToSelection(item)
    }
  }

  public items = new ObservableValue<T[]>([])

  public focusedItem = new ObservableValue<T | undefined>(undefined)

  /**
   * Stores the focused item captured on pointerdown, before the focus event
   * can update focusedItem. Used as the anchor for SHIFT+click range selection.
   * Call {@link setFocusAnchor} from `onpointerdown` to snapshot the anchor
   * before focus shifts.
   */
  private focusAnchor: T | undefined = undefined

  /** Snapshot the current focused item as the anchor for SHIFT+click range selection. */
  public setFocusAnchor(): void {
    this.focusAnchor = this.focusedItem.getValue()
  }

  public selection = new ObservableValue<T[]>([])

  public searchTerm = new ObservableValue('')

  public hasFocus = new ObservableValue(false)

  public handleKeyDown(ev: KeyboardEvent) {
    const items = this.items.getValue()
    const hasFocus = this.hasFocus.getValue()
    const selectedItems = this.selection.getValue()
    const focusedItem = this.focusedItem.getValue()
    const searchTerm = this.searchTerm.getValue()

    if (hasFocus) {
      switch (ev.key) {
        case ' ':
          ev.preventDefault()
          if (focusedItem) {
            this.selection.setValue(
              selectedItems.includes(focusedItem)
                ? selectedItems.filter((e) => e !== focusedItem)
                : [...selectedItems, focusedItem],
            )
          }
          break
        case '*':
          ev.preventDefault()
          this.selection.setValue(items.filter((e) => !selectedItems.includes(e)))
          break
        case '+':
          ev.preventDefault()
          this.selection.setValue(items)
          break
        case '-':
          ev.preventDefault()
          this.selection.setValue([])
          break
        case 'Insert':
          ev.preventDefault()
          if (focusedItem) {
            if (this.selection.getValue().includes(focusedItem)) {
              this.selection.setValue([...this.selection.getValue().filter((e) => e !== focusedItem)])
            } else {
              this.selection.setValue([...this.selection.getValue(), focusedItem])
            }
            this.focusedItem.setValue(items[items.findIndex((e) => e === this.focusedItem.getValue()) + 1])
          }
          break
        case 'Home': {
          ev.preventDefault()
          this.focusedItem.setValue(items[0])
          break
        }
        case 'End': {
          ev.preventDefault()
          this.focusedItem.setValue(items[items.length - 1])
          break
        }
        case 'Escape': {
          ev.preventDefault()
          this.searchTerm.setValue('')
          this.selection.setValue([])
          break
        }
        default:
          if (this.options.searchField && ev.key.length === 1) {
            const newSearchExpression = searchTerm + ev.key
            const newFocusedItem = items.find(
              (e) =>
                this.options.searchField &&
                (e[this.options.searchField] as string)?.toString().startsWith(newSearchExpression),
            )
            this.focusedItem.setValue(newFocusedItem)
            this.searchTerm.setValue(newSearchExpression)
          }
      }
    }
  }

  public handleItemClick(item: T, ev: MouseEvent) {
    const currentSelectionValue = this.selection.getValue()
    const lastFocused = this.focusAnchor ?? this.focusedItem.getValue()
    this.focusAnchor = undefined
    if (ev.ctrlKey) {
      if (currentSelectionValue.includes(item)) {
        this.selection.setValue(currentSelectionValue.filter((s) => s !== item))
      } else {
        this.selection.setValue([...currentSelectionValue, item])
      }
    }
    if (ev.shiftKey) {
      const items = this.items.getValue()
      const lastFocusedIndex = items.findIndex((e) => e === lastFocused)
      const itemIndex = items.findIndex((e) => e === item)
      const start = Math.min(lastFocusedIndex, itemIndex)
      const end = Math.max(lastFocusedIndex, itemIndex)
      const rangeItems = items.slice(start, end + 1)
      const newSelection = [...currentSelectionValue]
      for (const rangeItem of rangeItems) {
        if (!newSelection.includes(rangeItem)) {
          newSelection.push(rangeItem)
        }
      }
      this.selection.setValue(newSelection)
    }
    this.focusedItem.setValue(item)
  }

  /**
   * Hook for double-click behavior. No-op in base class; overridden by TreeService for expand/collapse.
   */
  public handleItemDoubleClick(_item: T) {}

  constructor(private options: ListServiceOptions<T> = {}) {}
}
