/**
 * Lightweight global keyboard shortcut handler.
 *
 * Two shortcut flavours are supported:
 *   - Plain keys: '?', '/', 'Escape' fire on keydown of that key.
 *   - Sequences:  'g d' fires after the user presses 'g' followed by
 *                 'd' within SEQUENCE_TIMEOUT_MS. The 'g' prefix is
 *                 reset if no second key arrives in time, so typing
 *                 'g' alone (e.g. into a future textbox) is safe.
 *
 * Shortcuts are suppressed when the user is typing in an input,
 * textarea, or contenteditable element (or when any modifier key is
 * held) — that is the entire point of `data-shortcut="false"` being
 * the default and the hook reading `event.target`.
 *
 * The hook owns no state and never throws. Components opt in by
 * calling `useKeyboardShortcuts(map)`. The `map` is an object whose
 * keys are shortcut strings and values are callbacks. Use
 * `[shortcut, callback]` tuple form when the callback would close
 * over changing values, to avoid registering a new listener on every
 * render.
 */
import { useEffect, useRef } from 'react'

const SEQUENCE_TIMEOUT_MS = 1500

export type ShortcutMap = Record<string, () => void>

function isEditableTarget(target: EventTarget | null): boolean {
  if (!(target instanceof HTMLElement)) return false
  const tag = target.tagName
  if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true
  if (target.isContentEditable) return true
  return false
}

function normaliseKey(e: KeyboardEvent): string | null {
  // Ignore modifier-key chords (Ctrl+R, Cmd+L, etc.) — those belong to
  // the browser, OS, or other registered handlers.
  if (e.ctrlKey || e.metaKey || e.altKey) return null
  const k = e.key
  if (k === ' ' || k === 'Tab' || k === 'Enter') return null
  return k.length === 1 ? k.toLowerCase() : k
}

export function useKeyboardShortcuts(map: ShortcutMap): void {
  // Use a ref so the listener (registered once) always sees the latest
  // callbacks without re-binding on every render.
  const mapRef = useRef(map)
  mapRef.current = map

  useEffect(() => {
    let pendingPrefix: string | null = null
    let pendingTimer: ReturnType<typeof setTimeout> | null = null

    const clearPending = () => {
      pendingPrefix = null
      if (pendingTimer !== null) {
        clearTimeout(pendingTimer)
        pendingTimer = null
      }
    }

    const onKeyDown = (e: KeyboardEvent) => {
      if (isEditableTarget(e.target)) return
      const k = normaliseKey(e)
      if (k === null) return

      // 1) Try the full key first (handles '?', 'Escape', '/', and
      //    any 2-key sequence the caller registered as a single string).
      const full = pendingPrefix ? `${pendingPrefix} ${k}` : k
      const cb = mapRef.current[full] ?? (pendingPrefix ? null : mapRef.current[k])
      if (cb) {
        e.preventDefault()
        cb()
        clearPending()
        return
      }

      // 2) Otherwise, if this key is itself a registered prefix
      //    (e.g. 'g' with map having 'g d', 'g r', ...), arm the
      //    sequence and wait for the next key.
      const isPrefix = Object.keys(mapRef.current).some(
        (key) => key.length > 1 && key.startsWith(`${k} `) && !key.includes('  '),
      )
      if (isPrefix && !pendingPrefix) {
        e.preventDefault()
        pendingPrefix = k
        pendingTimer = setTimeout(clearPending, SEQUENCE_TIMEOUT_MS)
        return
      }

      // Unrecognised key — drop any pending prefix silently.
      clearPending()
    }

    window.addEventListener('keydown', onKeyDown)
    return () => {
      window.removeEventListener('keydown', onKeyDown)
      clearPending()
    }
  }, [])
}
