/**
 * Popover - Rich content popup on click
 * @countermeasure/web-components/display/popover
 */

import { uniqueId } from '../../primitives/utils'
import DOMPurify from 'dompurify'
import { type PopoverConfig, type PopoverPlacement } from './types'

export type { PopoverConfig, PopoverPlacement } from './types'

/** Query all focusable elements within a container */
function getFocusableElements(container: HTMLElement): HTMLElement[] {
  const selector = [
    'a[href]',
    'button:not([disabled])',
    'input:not([disabled])',
    'select:not([disabled])',
    'textarea:not([disabled])',
    '[tabindex]:not([tabindex="-1"])',
  ].join(', ')

  return Array.from(container.querySelectorAll<HTMLElement>(selector))
}

/** Gap in pixels between trigger and popover */
const POPOVER_GAP = 8

export class Popover {
  private readonly trigger: HTMLElement
  private readonly placement: PopoverPlacement
  private readonly closeOnClickOutside: boolean
  private readonly closeOnEscape: boolean
  private readonly className: string | undefined
  private readonly onOpenCallback: (() => void) | undefined
  private readonly onCloseCallback: (() => void) | undefined
  private readonly popoverId: string

  private popoverEl: HTMLElement | null = null
  private contentEl: HTMLElement | null = null
  private content: HTMLElement | string
  private opened = false
  private destroyed = false

  /** Bound event handlers for cleanup */
  private readonly handleTriggerClick: () => void
  private readonly handleDocumentClick: (e: MouseEvent) => void
  private readonly handleKeyDown: (e: KeyboardEvent) => void

  constructor(config: PopoverConfig) {
    this.trigger = config.trigger
    this.content = config.content
    this.placement = config.placement ?? 'bottom'
    this.closeOnClickOutside = config.closeOnClickOutside !== false
    this.closeOnEscape = config.closeOnEscape !== false
    this.className = config.className
    this.onOpenCallback = config.onOpen
    this.onCloseCallback = config.onClose
    this.popoverId = uniqueId('cmm-popover')

    this.handleTriggerClick = () => {
      this.toggle()
    }

    this.handleDocumentClick = (e: MouseEvent) => {
      const target = e.target
      if (
        target instanceof Node &&
        this.popoverEl &&
        !this.popoverEl.contains(target) &&
        !this.trigger.contains(target)
      ) {
        this.close()
      }
    }

    this.handleKeyDown = (e: KeyboardEvent) => {
      if (e.key === 'Escape') {
        e.preventDefault()
        this.close()
        this.trigger.focus()
      }
    }

    this.trigger.setAttribute('aria-haspopup', 'dialog')
    this.trigger.setAttribute('aria-expanded', 'false')
    this.trigger.addEventListener('click', this.handleTriggerClick)
  }

  /** Check whether the popover is currently open */
  isOpen(): boolean {
    return this.opened
  }

  /** Open the popover */
  open(): void {
    if (this.destroyed || this.opened) {
      return
    }

    this.opened = true
    this.createPopoverElement()
    this.positionPopover()

    if (this.popoverEl) {
      // Force reflow before adding open class for transition
      void this.popoverEl.offsetHeight
      this.popoverEl.classList.add('cmm-popover--open')
    }

    this.trigger.setAttribute('aria-expanded', 'true')

    // Add document listeners
    if (this.closeOnClickOutside) {
      // Delay adding click listener to avoid immediate close from the same click
      setTimeout(() => {
        document.addEventListener('click', this.handleDocumentClick)
      }, 0)
    }

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

    // Focus first focusable element inside popover
    if (this.popoverEl) {
      const focusable = getFocusableElements(this.popoverEl)
      const first = focusable[0]
      if (first) {
        first.focus()
      } else {
        this.popoverEl.setAttribute('tabindex', '-1')
        this.popoverEl.focus()
      }
    }

    this.onOpenCallback?.()
  }

  /** Close the popover */
  close(): void {
    if (!this.opened) {
      return
    }

    this.opened = false
    this.trigger.setAttribute('aria-expanded', 'false')

    document.removeEventListener('click', this.handleDocumentClick)
    document.removeEventListener('keydown', this.handleKeyDown)

    if (this.popoverEl) {
      this.popoverEl.classList.remove('cmm-popover--open')

      const el = this.popoverEl
      // Remove after transition
      setTimeout(() => {
        el.remove()
      }, 200)

      this.popoverEl = null
      this.contentEl = null
    }

    this.onCloseCallback?.()
  }

  /** Toggle the popover open/closed */
  toggle(): void {
    if (this.opened) {
      this.close()
    } else {
      this.open()
    }
  }

  /** Update the popover content */
  setContent(content: HTMLElement | string): void {
    this.content = content
    if (this.contentEl) {
      this.renderContent()
    }
  }

  /** Remove all listeners and clean up */
  destroy(): void {
    if (this.destroyed) {
      return
    }
    this.destroyed = true

    this.close()

    this.trigger.removeEventListener('click', this.handleTriggerClick)
    this.trigger.removeAttribute('aria-haspopup')
    this.trigger.removeAttribute('aria-expanded')
  }

  /** Create the popover DOM element and append to body */
  private createPopoverElement(): void {
    // Remove existing popover if present
    if (this.popoverEl) {
      this.popoverEl.remove()
    }

    const popover = document.createElement('div')
    popover.className = 'cmm-popover'
    popover.id = this.popoverId

    if (this.className) {
      popover.classList.add(this.className)
    }

    // Content container
    const contentEl = document.createElement('div')
    contentEl.className = 'cmm-popover__content'
    popover.appendChild(contentEl)

    document.body.appendChild(popover)

    this.popoverEl = popover
    this.contentEl = contentEl

    this.renderContent()
  }

  /** Render the content into the content element */
  private renderContent(): void {
    if (!this.contentEl) {
      return
    }

    this.contentEl.innerHTML = ''

    if (typeof this.content === 'string') {
      this.contentEl.innerHTML = DOMPurify.sanitize(this.content)
    } else {
      this.contentEl.appendChild(this.content)
    }
  }

  /** Position the popover relative to the trigger element */
  private positionPopover(): void {
    if (!this.popoverEl) {
      return
    }

    const triggerRect = this.trigger.getBoundingClientRect()
    const popoverRect = this.popoverEl.getBoundingClientRect()
    const viewportWidth = window.innerWidth
    const viewportHeight = window.innerHeight

    let top = 0
    let left = 0

    const basePlacement = this.placement.split('-')[0] as
      | 'top'
      | 'bottom'
      | 'left'
      | 'right'
      | undefined
    const alignment = this.placement.includes('-')
      ? (this.placement.split('-')[1] as 'start' | 'end' | undefined)
      : undefined

    const effectivePlacement = basePlacement ?? 'bottom'

    switch (effectivePlacement) {
      case 'top': {
        top = triggerRect.top - popoverRect.height - POPOVER_GAP
        left = this.getAlignedLeft(triggerRect, popoverRect, alignment)
        break
      }
      case 'bottom': {
        top = triggerRect.bottom + POPOVER_GAP
        left = this.getAlignedLeft(triggerRect, popoverRect, alignment)
        break
      }
      case 'left': {
        top = triggerRect.top + (triggerRect.height - popoverRect.height) / 2
        left = triggerRect.left - popoverRect.width - POPOVER_GAP
        break
      }
      case 'right': {
        top = triggerRect.top + (triggerRect.height - popoverRect.height) / 2
        left = triggerRect.right + POPOVER_GAP
        break
      }
    }

    // Auto-flip vertically if not enough space
    if (effectivePlacement === 'top' && top < 0) {
      top = triggerRect.bottom + POPOVER_GAP
    } else if (effectivePlacement === 'bottom' && top + popoverRect.height > viewportHeight) {
      top = triggerRect.top - popoverRect.height - POPOVER_GAP
    }

    // Clamp to viewport
    left = Math.max(4, Math.min(left, viewportWidth - popoverRect.width - 4))
    top = Math.max(4, Math.min(top, viewportHeight - popoverRect.height - 4))

    this.popoverEl.style.left = `${String(left)}px`
    this.popoverEl.style.top = `${String(top)}px`
  }

  /** Get the left position based on alignment */
  private getAlignedLeft(
    triggerRect: DOMRect,
    popoverRect: DOMRect,
    alignment: 'start' | 'end' | undefined
  ): number {
    if (alignment === 'start') {
      return triggerRect.left
    }
    if (alignment === 'end') {
      return triggerRect.right - popoverRect.width
    }
    return triggerRect.left + (triggerRect.width - popoverRect.width) / 2
  }
}
