/**
 * Global keyboard shortcut registry
 * @countermeasure/web-components/services/keybindings
 */

export interface KeybindingEntry {
  keys: string
  handler: () => void
  description: string
  ignoreWhenEditing?: boolean
}

interface ParsedKey {
  ctrl: boolean
  shift: boolean
  alt: boolean
  meta: boolean
  key: string
}

interface InternalBinding {
  parsed: ParsedKey[]
  entry: KeybindingEntry
  groupId?: string
}

const EDITABLE_TAGS = new Set(['INPUT', 'TEXTAREA', 'SELECT'])

function isEditingElement(element: Element | null): boolean {
  if (!element) {
    return false
  }

  if (EDITABLE_TAGS.has(element.tagName)) {
    return true
  }

  if (element.getAttribute('contenteditable') === 'true') {
    return true
  }

  if (element.classList.contains('cm-content')) {
    return true
  }

  return false
}

function parseKeyCombo(combo: string): ParsedKey {
  const parts = combo.toLowerCase().split('+')
  const result: ParsedKey = {
    ctrl: false,
    shift: false,
    alt: false,
    meta: false,
    key: '',
  }

  for (const part of parts) {
    const trimmed = part.trim()
    if (trimmed === 'ctrl') {
      result.ctrl = true
    } else if (trimmed === 'shift') {
      result.shift = true
    } else if (trimmed === 'alt') {
      result.alt = true
    } else if (trimmed === 'meta' || trimmed === 'cmd') {
      result.meta = true
    } else {
      result.key = trimmed
    }
  }

  return result
}

function parseKeys(keys: string): ParsedKey[] {
  const chordParts = keys.split(' ').filter(s => s.trim().length > 0)
  return chordParts.map(parseKeyCombo)
}

function matchesKey(event: KeyboardEvent, parsed: ParsedKey): boolean {
  if (event.ctrlKey !== parsed.ctrl) {
    return false
  }
  if (event.shiftKey !== parsed.shift) {
    return false
  }
  if (event.altKey !== parsed.alt) {
    return false
  }
  if (event.metaKey !== parsed.meta) {
    return false
  }

  const eventKey = event.key.toLowerCase()
  const targetKey = parsed.key

  if (targetKey === 'enter') {
    return eventKey === 'enter'
  }
  if (targetKey === 'escape' || targetKey === 'esc') {
    return eventKey === 'escape'
  }
  if (targetKey === 'space') {
    return eventKey === ' '
  }
  if (targetKey === 'tab') {
    return eventKey === 'tab'
  }
  if (targetKey === 'backspace') {
    return eventKey === 'backspace'
  }
  if (targetKey === 'delete') {
    return eventKey === 'delete'
  }

  return eventKey === targetKey
}

const CHORD_TIMEOUT_MS = 500

export class KeybindingManager {
  private readonly bindings: InternalBinding[] = []
  private pendingChord: { parsed: ParsedKey; time: number } | null = null
  private chordTimer: ReturnType<typeof setTimeout> | null = null
  private destroyed = false

  private readonly handleKeyDown = (event: KeyboardEvent): void => {
    if (this.destroyed) {
      return
    }

    const editing = isEditingElement(document.activeElement)

    for (const binding of this.bindings) {
      if (binding.entry.ignoreWhenEditing === true && editing) {
        continue
      }

      const parsedSequence = binding.parsed
      const firstParsed = parsedSequence[0]

      if (!firstParsed) {
        continue
      }

      if (parsedSequence.length === 1) {
        if (this.pendingChord !== null) {
          continue
        }
        if (matchesKey(event, firstParsed)) {
          event.preventDefault()
          binding.entry.handler()
          return
        }
      } else if (parsedSequence.length === 2) {
        const secondParsed = parsedSequence[1]
        if (!secondParsed) {
          continue
        }

        if (this.pendingChord !== null) {
          if (
            matchesKey(event, secondParsed) &&
            matchesKeyExact(this.pendingChord.parsed, firstParsed)
          ) {
            this.clearChord()
            event.preventDefault()
            binding.entry.handler()
            return
          }
        } else if (matchesKey(event, firstParsed)) {
          this.startChord(firstParsed)
          event.preventDefault()
          return
        }
      }
    }

    if (this.pendingChord !== null) {
      this.clearChord()
    }
  }

  constructor() {
    document.addEventListener('keydown', this.handleKeyDown)
  }

  register(
    keys: string,
    handler: () => void,
    description: string,
    options?: { ignoreWhenEditing?: boolean }
  ): () => void {
    const parsed = parseKeys(keys)
    const ignoreWhenEditing = options?.ignoreWhenEditing
    const entry: KeybindingEntry =
      ignoreWhenEditing !== undefined
        ? { keys, handler, description, ignoreWhenEditing }
        : { keys, handler, description }
    const binding: InternalBinding = { parsed, entry }
    this.bindings.push(binding)

    return () => {
      const idx = this.bindings.indexOf(binding)
      if (idx !== -1) {
        this.bindings.splice(idx, 1)
      }
    }
  }

  registerGroup(groupId: string, bindings: [string, () => void, string][]): () => void {
    for (const [keys, handler, description] of bindings) {
      const parsed = parseKeys(keys)
      const entry: KeybindingEntry = { keys, handler, description }
      this.bindings.push({ parsed, entry, groupId })
    }

    return () => {
      this.unregisterGroup(groupId)
    }
  }

  unregisterGroup(groupId: string): void {
    for (let i = this.bindings.length - 1; i >= 0; i -= 1) {
      const binding = this.bindings[i]
      if (binding !== undefined && binding.groupId === groupId) {
        this.bindings.splice(i, 1)
      }
    }
  }

  getBindings(): KeybindingEntry[] {
    return this.bindings.map(b => ({ ...b.entry }))
  }

  destroy(): void {
    this.destroyed = true
    this.clearChord()
    document.removeEventListener('keydown', this.handleKeyDown)
    this.bindings.length = 0
  }

  private startChord(parsed: ParsedKey): void {
    this.clearChord()
    this.pendingChord = { parsed, time: Date.now() }
    this.chordTimer = setTimeout(() => {
      this.pendingChord = null
      this.chordTimer = null
    }, CHORD_TIMEOUT_MS)
  }

  private clearChord(): void {
    this.pendingChord = null
    if (this.chordTimer !== null) {
      clearTimeout(this.chordTimer)
      this.chordTimer = null
    }
  }
}

function matchesKeyExact(a: ParsedKey, b: ParsedKey): boolean {
  return (
    a.ctrl === b.ctrl &&
    a.shift === b.shift &&
    a.alt === b.alt &&
    a.meta === b.meta &&
    a.key === b.key
  )
}
