/**
 * useKeybindings — React hook wrapping KeybindingManager.
 *
 * Registers one or more keybindings on mount and deregisters them on unmount
 * or when the `enabled` flag turns false.  A new KeybindingManager is
 * instantiated inside the hook; consumers that need a shared manager should
 * create one externally and call `manager.register()` directly.
 *
 * Handlers are routed through a stable ref trampoline so callers can pass
 * arrow functions created in the render body without causing bindings to
 * re-register on every render.
 */
import { useEffect, useRef } from 'react'
import { KeybindingManager } from '../../services/keybindings'
import type { KeybindingEntry } from '../../services/keybindings'

export type { KeybindingEntry }

export interface UseKeybindingsOptions {
  /** When false, bindings are deregistered until re-enabled. Default: true. */
  enabled?: boolean
  /** Logical group identifier — passed to `registerGroup`. */
  groupId?: string
}

/**
 * @param bindings - One or more KeybindingEntry descriptors to register.
 * @param options  - Optional configuration.
 */
export function useKeybindings(
  bindings: KeybindingEntry | KeybindingEntry[],
  options?: UseKeybindingsOptions
): void {
  const { enabled = true, groupId } = options ?? {}

  // Normalise to array and keep in a ref so that identity changes in the
  // bindings array don't force a re-run of the effect.
  const bindingsArray = Array.isArray(bindings) ? bindings : [bindings]

  // Keep latest bindings ref-current so the trampoline uses the up-to-date
  // handler without needing to re-register.
  const bindingsRef = useRef<KeybindingEntry[]>(bindingsArray)
  bindingsRef.current = bindingsArray

  // Stable manager per component lifetime.
  const managerRef = useRef<KeybindingManager | null>(null)
  if (managerRef.current === null) {
    managerRef.current = new KeybindingManager()
  }

  useEffect(() => {
    const mgr = managerRef.current
    if (!mgr || !enabled) {
      return
    }

    const cleanups: Array<() => void> = []

    for (const entry of bindingsRef.current) {
      // Trampoline: register a stable wrapper that delegates to the ref.
      const stableHandler = (): void => {
        // Find matching entry by keys to call the latest handler.
        const live = bindingsRef.current.find(b => b.keys === entry.keys)
        live?.handler()
      }

      const unregister =
        entry.ignoreWhenEditing !== undefined
          ? mgr.register(entry.keys, stableHandler, entry.description, {
              ignoreWhenEditing: entry.ignoreWhenEditing,
            })
          : mgr.register(entry.keys, stableHandler, entry.description)
      cleanups.push(unregister)
    }

    if (groupId) {
      // Also tag the group so it can be bulk-removed.
      const tuples: [string, () => void, string][] = bindingsRef.current.map(e => {
        const h = (): void => {
          const live = bindingsRef.current.find(b => b.keys === e.keys)
          live?.handler()
        }
        return [e.keys, h, e.description]
      })
      // Note: registerGroup adds on top of the per-binding registrations above,
      // so we skip the per-binding loop when groupId is given and use only group.
      for (const cleanup of cleanups) cleanup()
      cleanups.length = 0
      const unregisterGroup = mgr.registerGroup(groupId, tuples)
      cleanups.push(unregisterGroup)
    }

    return () => {
      for (const cleanup of cleanups) cleanup()
    }
    // enabled controls registration; re-run when it changes.
    // bindingsRef.current is intentionally excluded — the trampoline avoids
    // re-registration when only the handler identity changes.
  }, [enabled, groupId])

  // Destroy manager on final unmount.
  useEffect(() => {
    return () => {
      managerRef.current?.destroy()
      managerRef.current = null
    }
  }, [])
}
