/**
 * Accessibility utilities for web components
 * @countermeasure/web-components/utils/a11y
 *
 * Provides focus trap, live region announcements, roving tabindex,
 * and ARIA linking helpers for WCAG 2.1 AA compliance.
 */

// ============================================================================
// Types
// ============================================================================

export interface FocusTrap {
  /** Activate the focus trap */
  activate: () => void
  /** Deactivate the focus trap */
  deactivate: () => void
  /** Update focusable elements (call after DOM changes) */
  update: () => void
  /** Check if focus trap is active */
  isActive: () => boolean
}

export interface LiveRegion {
  /** Announce a message to screen readers */
  announce: (message: string) => void
  /** Clear the live region content */
  clear: () => void
  /** Remove the live region from DOM */
  destroy: () => void
}

export interface RovingTabindex {
  /** Move focus to the next item */
  next: () => void
  /** Move focus to the previous item */
  prev: () => void
  /** Move focus to the first item */
  first: () => void
  /** Move focus to the last item */
  last: () => void
  /** Get the currently focused index */
  getCurrentIndex: () => number
  /** Set focus to a specific index */
  setIndex: (index: number) => void
  /** Update items (call after DOM changes) */
  update: () => void
  /** Clean up event listeners */
  destroy: () => void
}

// ============================================================================
// Focus Trap
// ============================================================================

/** Selector for all focusable elements */
const FOCUSABLE_SELECTOR = [
  'a[href]',
  'button:not([disabled])',
  'input:not([disabled])',
  'select:not([disabled])',
  'textarea:not([disabled])',
  '[tabindex]:not([tabindex="-1"])',
  '[contenteditable="true"]',
].join(', ')

/**
 * Get all focusable elements within a container
 */
export function getFocusableElements(container: HTMLElement): HTMLElement[] {
  return Array.from(container.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR)).filter(el => {
    // Exclude hidden elements
    if (el.offsetParent === null && el.getAttribute('aria-hidden') !== 'false') {
      return false
    }
    // Exclude elements with aria-hidden="true"
    if (el.getAttribute('aria-hidden') === 'true') {
      return false
    }
    return true
  })
}

/**
 * Create a focus trap within a container element.
 * Traps Tab/Shift+Tab navigation within the container.
 */
export function createFocusTrap(container: HTMLElement): FocusTrap {
  let focusableElements: HTMLElement[] = []
  let active = false
  let previouslyFocused: HTMLElement | null = null

  const handleKeyDown = (event: KeyboardEvent): void => {
    if (event.key !== 'Tab' || focusableElements.length === 0) {
      return
    }

    const firstElement = focusableElements[0]
    const lastElement = focusableElements[focusableElements.length - 1]

    if (firstElement === undefined || lastElement === undefined) {
      return
    }

    if (event.shiftKey) {
      // Shift+Tab
      if (document.activeElement === firstElement) {
        event.preventDefault()
        lastElement.focus()
      }
    } else {
      // Tab
      if (document.activeElement === lastElement) {
        event.preventDefault()
        firstElement.focus()
      }
    }
  }

  const update = (): void => {
    focusableElements = getFocusableElements(container)
  }

  const activate = (): void => {
    if (active) {
      return
    }
    active = true
    previouslyFocused = document.activeElement as HTMLElement | null
    update()
    document.addEventListener('keydown', handleKeyDown)

    // Focus the first focusable element or the container itself
    if (focusableElements.length > 0 && focusableElements[0] !== undefined) {
      focusableElements[0].focus()
    } else {
      container.focus()
    }
  }

  const deactivate = (): void => {
    if (!active) {
      return
    }
    active = false
    document.removeEventListener('keydown', handleKeyDown)

    // Restore focus to previously focused element
    if (previouslyFocused !== null && document.body.contains(previouslyFocused)) {
      previouslyFocused.focus()
    }
  }

  return {
    activate,
    deactivate,
    update,
    isActive: () => active,
  }
}

// ============================================================================
// Live Region Announcements
// ============================================================================

/** Global live region elements for polite and assertive announcements */
let politeLiveRegion: HTMLElement | null = null
let assertiveLiveRegion: HTMLElement | null = null

/**
 * Get or create a global live region element
 */
function getGlobalLiveRegion(politeness: 'polite' | 'assertive'): HTMLElement {
  const existing = politeness === 'polite' ? politeLiveRegion : assertiveLiveRegion

  if (existing !== null && document.body.contains(existing)) {
    return existing
  }

  const region = document.createElement('div')
  region.setAttribute('role', 'status')
  region.setAttribute('aria-live', politeness)
  region.setAttribute('aria-atomic', 'true')
  region.className = 'cmm-sr-only cmm-live-region'
  region.style.cssText = `
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  `

  document.body.appendChild(region)

  if (politeness === 'polite') {
    politeLiveRegion = region
  } else {
    assertiveLiveRegion = region
  }

  return region
}

/**
 * Announce a message to screen readers via ARIA live region.
 *
 * @param message - The message to announce
 * @param politeness - 'polite' waits for idle, 'assertive' interrupts immediately
 */
export function announce(message: string, politeness: 'polite' | 'assertive' = 'polite'): void {
  const region = getGlobalLiveRegion(politeness)

  // Clear and re-set content to trigger announcement
  region.textContent = ''

  // Use requestAnimationFrame to ensure the clear happens before the new content
  requestAnimationFrame(() => {
    region.textContent = message
  })
}

/**
 * Create a local live region for component-specific announcements.
 */
export function createLiveRegion(politeness: 'polite' | 'assertive' = 'polite'): LiveRegion {
  const region = document.createElement('div')
  region.setAttribute('role', 'status')
  region.setAttribute('aria-live', politeness)
  region.setAttribute('aria-atomic', 'true')
  region.className = 'cmm-sr-only cmm-live-region'
  region.style.cssText = `
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  `

  return {
    announce: (message: string) => {
      // Ensure region is in the DOM
      if (!document.body.contains(region)) {
        document.body.appendChild(region)
      }
      region.textContent = ''
      requestAnimationFrame(() => {
        region.textContent = message
      })
    },
    clear: () => {
      region.textContent = ''
    },
    destroy: () => {
      region.remove()
    },
  }
}

// ============================================================================
// Roving Tabindex
// ============================================================================

/**
 * Create a roving tabindex for keyboard navigation within a widget.
 * Only one item is focusable at a time (tabindex="0"), others are tabindex="-1".
 *
 * @param container - The container element
 * @param selector - CSS selector for navigable items
 * @param options - Configuration options
 */
export function createRovingTabindex(
  container: HTMLElement,
  selector: string,
  options?: {
    /** Initial focused index (default: 0) */
    initialIndex?: number
    /** Whether to wrap around at ends (default: true) */
    wrap?: boolean
    /** Whether navigation is horizontal or vertical (default: 'vertical') */
    orientation?: 'horizontal' | 'vertical' | 'both'
    /** Callback when focus changes */
    onFocusChange?: (index: number, element: HTMLElement) => void
  }
): RovingTabindex {
  let items: HTMLElement[] = []
  let currentIndex = options?.initialIndex ?? 0
  const wrap = options?.wrap ?? true
  const orientation = options?.orientation ?? 'vertical'
  const onFocusChange = options?.onFocusChange

  const update = (): void => {
    items = Array.from(container.querySelectorAll<HTMLElement>(selector))
    // Reset tabindex on all items
    for (const item of items) {
      item.setAttribute('tabindex', '-1')
    }
    // Set tabindex="0" on current item
    const currentItem = items[currentIndex]
    if (currentItem !== undefined) {
      currentItem.setAttribute('tabindex', '0')
    } else if (items.length > 0) {
      const firstItem = items[0]
      if (firstItem !== undefined) {
        currentIndex = 0
        firstItem.setAttribute('tabindex', '0')
      }
    }
  }

  const focusItem = (index: number): void => {
    if (items[index] === undefined) {
      return
    }

    // Update tabindex
    const previousItem = items[currentIndex]
    if (previousItem !== undefined) {
      previousItem.setAttribute('tabindex', '-1')
    }

    currentIndex = index
    const newItem = items[currentIndex]
    if (newItem !== undefined) {
      newItem.setAttribute('tabindex', '0')
      newItem.focus()
      onFocusChange?.(currentIndex, newItem)
    }
  }

  const next = (): void => {
    if (items.length === 0) {
      return
    }
    let nextIndex = currentIndex + 1
    if (nextIndex >= items.length) {
      nextIndex = wrap ? 0 : items.length - 1
    }
    focusItem(nextIndex)
  }

  const prev = (): void => {
    if (items.length === 0) {
      return
    }
    let prevIndex = currentIndex - 1
    if (prevIndex < 0) {
      prevIndex = wrap ? items.length - 1 : 0
    }
    focusItem(prevIndex)
  }

  const first = (): void => {
    focusItem(0)
  }

  const last = (): void => {
    focusItem(items.length - 1)
  }

  const handleKeyDown = (event: KeyboardEvent): void => {
    const target = event.target as HTMLElement
    if (!items.includes(target)) {
      return
    }

    let handled = false

    switch (event.key) {
      case 'ArrowDown':
        if (orientation === 'vertical' || orientation === 'both') {
          next()
          handled = true
        }
        break
      case 'ArrowUp':
        if (orientation === 'vertical' || orientation === 'both') {
          prev()
          handled = true
        }
        break
      case 'ArrowRight':
        if (orientation === 'horizontal' || orientation === 'both') {
          next()
          handled = true
        }
        break
      case 'ArrowLeft':
        if (orientation === 'horizontal' || orientation === 'both') {
          prev()
          handled = true
        }
        break
      case 'Home':
        first()
        handled = true
        break
      case 'End':
        last()
        handled = true
        break
    }

    if (handled) {
      event.preventDefault()
    }
  }

  // Initialize
  update()
  container.addEventListener('keydown', handleKeyDown)

  return {
    next,
    prev,
    first,
    last,
    getCurrentIndex: () => currentIndex,
    setIndex: (index: number) => {
      focusItem(index)
    },
    update,
    destroy: () => {
      container.removeEventListener('keydown', handleKeyDown)
    },
  }
}

// ============================================================================
// ARIA Linking Helpers
// ============================================================================

/**
 * Link an element to its description using aria-describedby.
 * Appends to existing descriptions if present.
 */
export function linkDescribedBy(element: HTMLElement, descriptionId: string): void {
  const existing = element.getAttribute('aria-describedby')
  if (existing !== null) {
    // Avoid duplicates
    const ids = existing.split(' ')
    if (!ids.includes(descriptionId)) {
      element.setAttribute('aria-describedby', `${existing} ${descriptionId}`)
    }
  } else {
    element.setAttribute('aria-describedby', descriptionId)
  }
}

/**
 * Link an element to its label using aria-labelledby.
 * Appends to existing labels if present.
 */
export function linkLabelledBy(element: HTMLElement, labelId: string): void {
  const existing = element.getAttribute('aria-labelledby')
  if (existing !== null) {
    // Avoid duplicates
    const ids = existing.split(' ')
    if (!ids.includes(labelId)) {
      element.setAttribute('aria-labelledby', `${existing} ${labelId}`)
    }
  } else {
    element.setAttribute('aria-labelledby', labelId)
  }
}

/**
 * Set up a relationship between a control and a region it controls.
 */
export function linkControls(control: HTMLElement, regionId: string): void {
  control.setAttribute('aria-controls', regionId)
}

/**
 * Set up an owns relationship for virtual children.
 */
export function linkOwns(parent: HTMLElement, childIds: string[]): void {
  parent.setAttribute('aria-owns', childIds.join(' '))
}

// ============================================================================
// Screen Reader Only Content
// ============================================================================

/**
 * Create a visually hidden element for screen readers only.
 */
export function createSrOnlyElement(text: string): HTMLElement {
  const el = document.createElement('span')
  el.className = 'cmm-sr-only'
  el.style.cssText = `
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  `
  el.textContent = text
  return el
}

// ============================================================================
// Focus Visible Polyfill Support
// ============================================================================

/**
 * Check if focus-visible is natively supported.
 */
export function isFocusVisibleSupported(): boolean {
  try {
    document.querySelector(':focus-visible')
    return true
  } catch {
    return false
  }
}

/**
 * Add focus-visible class when element receives keyboard focus.
 * Falls back for browsers without native :focus-visible support.
 */
export function manageFocusVisible(element: HTMLElement): () => void {
  let hadKeyboardEvent = false

  const handleKeyDown = (): void => {
    hadKeyboardEvent = true
  }

  const handlePointerDown = (): void => {
    hadKeyboardEvent = false
  }

  const handleFocus = (): void => {
    if (hadKeyboardEvent) {
      element.classList.add('focus-visible')
    }
  }

  const handleBlur = (): void => {
    element.classList.remove('focus-visible')
    hadKeyboardEvent = false
  }

  document.addEventListener('keydown', handleKeyDown)
  document.addEventListener('pointerdown', handlePointerDown)
  element.addEventListener('focus', handleFocus)
  element.addEventListener('blur', handleBlur)

  return () => {
    document.removeEventListener('keydown', handleKeyDown)
    document.removeEventListener('pointerdown', handlePointerDown)
    element.removeEventListener('focus', handleFocus)
    element.removeEventListener('blur', handleBlur)
  }
}
