/**
 * Carousel - Content slider with touch, keyboard, and autoplay support
 * @countermeasure/web-components/overlays/carousel
 */

import { resolveContainer, uniqueId } from '../../primitives/utils'
import DOMPurify from 'dompurify'
import { type CarouselConfig, type CarouselSlide } from './types'

export type { CarouselConfig, CarouselSlide } from './types'

/** Minimum swipe distance in pixels to trigger slide change */
const SWIPE_THRESHOLD = 50

export class Carousel {
  private readonly containerEl: HTMLElement
  private readonly slides: CarouselSlide[]
  private readonly shouldLoop: boolean
  private readonly showArrows: boolean
  private readonly showDots: boolean
  private readonly pauseOnHover: boolean
  private readonly autoplayInterval: number
  private readonly onChangeCallback: ((index: number) => void) | undefined
  private readonly className: string | undefined
  private readonly carouselId: string

  private rootEl: HTMLElement | null = null
  private trackEl: HTMLElement | null = null
  private liveEl: HTMLElement | null = null
  private prevBtn: HTMLButtonElement | null = null
  private nextBtn: HTMLButtonElement | null = null
  private dotEls: HTMLButtonElement[] = []
  private currentIndex = 0
  private autoplayEnabled: boolean
  private autoplayTimer: ReturnType<typeof setInterval> | null = null
  private resizeObserver: ResizeObserver | null = null
  private destroyed = false

  /** Touch tracking */
  private touchStartX = 0
  private touchStartY = 0
  private isSwiping = false

  /** Bound event handlers for cleanup */
  private readonly handleKeyDown: (e: KeyboardEvent) => void
  private readonly handleTouchStart: (e: TouchEvent) => void
  private readonly handleTouchEnd: (e: TouchEvent) => void
  private readonly handleMouseEnter: () => void
  private readonly handleMouseLeave: () => void

  constructor(config: CarouselConfig) {
    this.containerEl = resolveContainer(config.container)
    this.slides = config.slides
    this.shouldLoop = config.loop ?? false
    this.showArrows = config.showArrows !== false
    this.showDots = config.showDots !== false
    this.pauseOnHover = config.pauseOnHover !== false
    this.autoplayEnabled = config.autoplay ?? false
    this.autoplayInterval = config.autoplayInterval ?? 5000
    this.onChangeCallback = config.onChange
    this.className = config.className
    this.carouselId = uniqueId('cmm-carousel')

    this.handleKeyDown = (e: KeyboardEvent) => {
      this.onKeyDown(e)
    }

    this.handleTouchStart = (e: TouchEvent) => {
      this.onTouchStart(e)
    }

    this.handleTouchEnd = (e: TouchEvent) => {
      this.onTouchEnd(e)
    }

    this.handleMouseEnter = () => {
      if (this.pauseOnHover && this.autoplayEnabled) {
        this.stopAutoplayTimer()
      }
    }

    this.handleMouseLeave = () => {
      if (this.pauseOnHover && this.autoplayEnabled) {
        this.startAutoplayTimer()
      }
    }

    this.render()

    if (this.autoplayEnabled) {
      this.startAutoplayTimer()
    }
  }

  /** Navigate to a specific slide by index */
  goTo(index: number): void {
    if (this.destroyed || this.slides.length === 0) {
      return
    }

    let targetIndex = index

    if (this.shouldLoop) {
      if (targetIndex < 0) {
        targetIndex = this.slides.length - 1
      } else if (targetIndex >= this.slides.length) {
        targetIndex = 0
      }
    } else {
      targetIndex = Math.max(0, Math.min(targetIndex, this.slides.length - 1))
    }

    if (targetIndex === this.currentIndex) {
      return
    }

    this.currentIndex = targetIndex
    this.updateTrackPosition()
    this.updateArrowState()
    this.updateDotState()
    this.updateLiveRegion()
    this.onChangeCallback?.(this.currentIndex)
  }

  /** Go to the next slide */
  next(): void {
    this.goTo(this.currentIndex + 1)
  }

  /** Go to the previous slide */
  prev(): void {
    this.goTo(this.currentIndex - 1)
  }

  /** Start autoplay */
  play(): void {
    this.autoplayEnabled = true
    this.startAutoplayTimer()
  }

  /** Stop autoplay */
  pause(): void {
    this.autoplayEnabled = false
    this.stopAutoplayTimer()
  }

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

    this.stopAutoplayTimer()

    if (this.resizeObserver) {
      this.resizeObserver.disconnect()
      this.resizeObserver = null
    }

    if (this.rootEl) {
      this.rootEl.removeEventListener('keydown', this.handleKeyDown)
      this.rootEl.removeEventListener('touchstart', this.handleTouchStart)
      this.rootEl.removeEventListener('touchend', this.handleTouchEnd)
      this.rootEl.removeEventListener('mouseenter', this.handleMouseEnter)
      this.rootEl.removeEventListener('mouseleave', this.handleMouseLeave)
      this.rootEl.remove()
      this.rootEl = null
    }

    this.trackEl = null
    this.liveEl = null
    this.prevBtn = null
    this.nextBtn = null
    this.dotEls = []
  }

  /** Build the carousel DOM structure */
  private render(): void {
    // Root element
    const root = document.createElement('div')
    root.className = 'cmm-carousel'
    root.id = this.carouselId
    root.setAttribute('role', 'region')
    root.setAttribute('aria-roledescription', 'carousel')
    root.setAttribute('aria-label', 'Content carousel')
    root.setAttribute('tabindex', '0')

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

    // Viewport
    const viewport = document.createElement('div')
    viewport.className = 'cmm-carousel__viewport'

    // Track
    const track = document.createElement('div')
    track.className = 'cmm-carousel__track'
    track.setAttribute('aria-live', 'off')
    this.trackEl = track

    // Slides
    for (let i = 0; i < this.slides.length; i++) {
      const slideData = this.slides[i]
      if (slideData === undefined) {
        continue
      }

      const slide = document.createElement('div')
      slide.className = 'cmm-carousel__slide'
      slide.setAttribute('role', 'group')
      slide.setAttribute('aria-roledescription', 'slide')
      slide.setAttribute(
        'aria-label',
        slideData.label ?? `Slide ${String(i + 1)} of ${String(this.slides.length)}`
      )

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

      track.appendChild(slide)
    }

    viewport.appendChild(track)
    root.appendChild(viewport)

    // Arrow buttons
    if (this.showArrows && this.slides.length > 1) {
      const prevBtn = document.createElement('button')
      prevBtn.className = 'cmm-carousel__arrow cmm-carousel__arrow--prev'
      prevBtn.type = 'button'
      prevBtn.setAttribute('aria-label', 'Previous slide')
      prevBtn.innerHTML = '&#8249;'
      prevBtn.addEventListener('click', () => {
        this.prev()
      })
      root.appendChild(prevBtn)
      this.prevBtn = prevBtn

      const nextBtn = document.createElement('button')
      nextBtn.className = 'cmm-carousel__arrow cmm-carousel__arrow--next'
      nextBtn.type = 'button'
      nextBtn.setAttribute('aria-label', 'Next slide')
      nextBtn.innerHTML = '&#8250;'
      nextBtn.addEventListener('click', () => {
        this.next()
      })
      root.appendChild(nextBtn)
      this.nextBtn = nextBtn
    }

    // Dot indicators
    if (this.showDots && this.slides.length > 1) {
      const dots = document.createElement('div')
      dots.className = 'cmm-carousel__dots'
      dots.setAttribute('role', 'tablist')
      dots.setAttribute('aria-label', 'Slide navigation')

      this.dotEls = []

      for (let i = 0; i < this.slides.length; i++) {
        const dot = document.createElement('button')
        dot.className = 'cmm-carousel__dot'
        dot.type = 'button'
        dot.setAttribute('role', 'tab')
        dot.setAttribute('aria-label', `Go to slide ${String(i + 1)}`)
        dot.setAttribute('aria-selected', i === 0 ? 'true' : 'false')

        const slideIndex = i
        dot.addEventListener('click', () => {
          this.goTo(slideIndex)
        })

        dots.appendChild(dot)
        this.dotEls.push(dot)
      }

      root.appendChild(dots)
    }

    // Live region for screen readers
    const live = document.createElement('div')
    live.className = 'cmm-carousel__live'
    live.setAttribute('aria-live', 'polite')
    live.setAttribute('aria-atomic', 'true')
    root.appendChild(live)
    this.liveEl = live

    this.rootEl = root

    // Event listeners
    root.addEventListener('keydown', this.handleKeyDown)
    root.addEventListener('touchstart', this.handleTouchStart, { passive: true })
    root.addEventListener('touchend', this.handleTouchEnd, { passive: true })
    root.addEventListener('mouseenter', this.handleMouseEnter)
    root.addEventListener('mouseleave', this.handleMouseLeave)

    // Append to container
    this.containerEl.appendChild(root)

    // Set initial state
    this.updateTrackPosition()
    this.updateArrowState()
    this.updateDotState()

    // Watch for resize to recalculate positions
    this.resizeObserver = new ResizeObserver(() => {
      this.updateTrackPosition()
    })
    this.resizeObserver.observe(root)
  }

  /** Update the track CSS transform to show the current slide */
  private updateTrackPosition(): void {
    if (!this.trackEl) {
      return
    }
    const offset = -(this.currentIndex * 100)
    this.trackEl.style.transform = `translateX(${String(offset)}%)`
  }

  /** Enable/disable arrow buttons based on current position */
  private updateArrowState(): void {
    if (this.shouldLoop) {
      return
    }

    if (this.prevBtn) {
      this.prevBtn.disabled = this.currentIndex === 0
    }

    if (this.nextBtn) {
      this.nextBtn.disabled = this.currentIndex === this.slides.length - 1
    }
  }

  /** Update dot active states */
  private updateDotState(): void {
    for (let i = 0; i < this.dotEls.length; i++) {
      const dot = this.dotEls[i]
      if (dot === undefined) {
        continue
      }

      if (i === this.currentIndex) {
        dot.classList.add('cmm-carousel__dot--active')
        dot.setAttribute('aria-selected', 'true')
      } else {
        dot.classList.remove('cmm-carousel__dot--active')
        dot.setAttribute('aria-selected', 'false')
      }
    }
  }

  /** Update the live region for screen reader announcements */
  private updateLiveRegion(): void {
    if (!this.liveEl) {
      return
    }

    const slideData = this.slides[this.currentIndex]
    const label =
      slideData?.label ?? `Slide ${String(this.currentIndex + 1)} of ${String(this.slides.length)}`
    this.liveEl.textContent = label
  }

  /** Start the autoplay interval timer */
  private startAutoplayTimer(): void {
    this.stopAutoplayTimer()

    if (this.slides.length <= 1) {
      return
    }

    this.autoplayTimer = setInterval(() => {
      if (this.shouldLoop) {
        this.next()
      } else if (this.currentIndex < this.slides.length - 1) {
        this.next()
      } else {
        this.stopAutoplayTimer()
      }
    }, this.autoplayInterval)
  }

  /** Stop the autoplay interval timer */
  private stopAutoplayTimer(): void {
    if (this.autoplayTimer !== null) {
      clearInterval(this.autoplayTimer)
      this.autoplayTimer = null
    }
  }

  /** Handle keyboard navigation */
  private onKeyDown(e: KeyboardEvent): void {
    if (e.key === 'ArrowLeft') {
      e.preventDefault()
      this.prev()
    } else if (e.key === 'ArrowRight') {
      e.preventDefault()
      this.next()
    } else if (e.key === 'Home') {
      e.preventDefault()
      this.goTo(0)
    } else if (e.key === 'End') {
      e.preventDefault()
      this.goTo(this.slides.length - 1)
    }
  }

  /** Handle touch start for swipe detection */
  private onTouchStart(e: TouchEvent): void {
    const touch = e.touches[0]
    if (touch === undefined) {
      return
    }
    this.touchStartX = touch.clientX
    this.touchStartY = touch.clientY
    this.isSwiping = true
  }

  /** Handle touch end for swipe detection */
  private onTouchEnd(e: TouchEvent): void {
    if (!this.isSwiping) {
      return
    }
    this.isSwiping = false

    const touch = e.changedTouches[0]
    if (touch === undefined) {
      return
    }

    const deltaX = touch.clientX - this.touchStartX
    const deltaY = touch.clientY - this.touchStartY

    // Only trigger if horizontal swipe is dominant
    if (Math.abs(deltaX) < SWIPE_THRESHOLD || Math.abs(deltaY) > Math.abs(deltaX)) {
      return
    }

    if (deltaX < 0) {
      this.next()
    } else {
      this.prev()
    }
  }
}
