/**
 * Slider - Range slider component
 * @countermeasure/web-components/primitives/slider
 */

import { type SliderConfig } from './types'
import { resolveContainer, uniqueId } from '../utils'

export type { SliderConfig } from './types'

export class Slider {
  public readonly el: HTMLDivElement

  private readonly trackEl: HTMLDivElement
  private readonly fillEl: HTMLDivElement
  private readonly thumbEl: HTMLDivElement
  private tooltipEl: HTMLDivElement | null = null
  private ticksEl: HTMLDivElement | null = null
  private readonly sliderId: string

  private min: number
  private max: number
  private step: number
  private currentValue: number
  private isDragging = false
  private readonly showTooltip: boolean
  private readonly showTicks: boolean

  private readonly onChangeCallback: ((value: number) => void) | undefined
  private readonly onChangeEndCallback: ((value: number) => void) | undefined

  // Bound event handlers for cleanup
  private readonly handlePointerDown: (e: PointerEvent) => void
  private readonly handlePointerMove: (e: PointerEvent) => void
  private readonly handlePointerUp: (e: PointerEvent) => void
  private readonly handleKeyDown: (e: KeyboardEvent) => void
  private readonly handleTrackClick: (e: PointerEvent) => void

  constructor(config: SliderConfig) {
    this.sliderId = uniqueId('cmm-slider')
    this.min = config.min ?? 0
    this.max = config.max ?? 100
    this.step = config.step ?? 1
    this.showTooltip = config.showTooltip === true
    this.showTicks = config.showTicks === true
    this.onChangeCallback = config.onChange
    this.onChangeEndCallback = config.onChangeEnd

    // Ensure valid range
    if (this.max <= this.min) {
      this.max = this.min + 1
    }

    // Clamp initial value
    this.currentValue = this.clamp(config.value ?? this.min)

    // Bind handlers
    this.handlePointerDown = this.onPointerDown.bind(this)
    this.handlePointerMove = this.onPointerMove.bind(this)
    this.handlePointerUp = this.onPointerUp.bind(this)
    this.handleKeyDown = this.onKeyDown.bind(this)
    this.handleTrackClick = this.onTrackClick.bind(this)

    // Create container
    this.el = document.createElement('div')
    this.el.className = this.buildClassName(config.className)
    this.el.id = this.sliderId

    // Create track
    this.trackEl = document.createElement('div')
    this.trackEl.className = 'cmm-slider__track'

    // Create fill
    this.fillEl = document.createElement('div')
    this.fillEl.className = 'cmm-slider__fill'

    // Create thumb
    this.thumbEl = document.createElement('div')
    this.thumbEl.className = 'cmm-slider__thumb'
    this.thumbEl.tabIndex = 0
    this.thumbEl.setAttribute('role', 'slider')
    this.thumbEl.setAttribute('aria-valuemin', String(this.min))
    this.thumbEl.setAttribute('aria-valuemax', String(this.max))
    this.thumbEl.setAttribute('aria-valuenow', String(this.currentValue))

    if (config.ariaLabel !== undefined) {
      this.thumbEl.setAttribute('aria-label', config.ariaLabel)
    }

    // Assemble track
    this.trackEl.appendChild(this.fillEl)
    this.trackEl.appendChild(this.thumbEl)
    this.el.appendChild(this.trackEl)

    // Optional tooltip
    if (this.showTooltip) {
      this.tooltipEl = document.createElement('div')
      this.tooltipEl.className = 'cmm-slider__tooltip'
      this.tooltipEl.textContent = String(this.currentValue)
      this.thumbEl.appendChild(this.tooltipEl)
    }

    // Optional tick marks
    if (this.showTicks) {
      this.buildTicks()
    }

    // Set initial position
    this.updatePosition()

    // Disabled state
    if (config.disabled === true) {
      this.setDisabled(true)
    }

    // Event listeners
    this.thumbEl.addEventListener('pointerdown', this.handlePointerDown)
    this.trackEl.addEventListener('pointerdown', this.handleTrackClick)
    this.thumbEl.addEventListener('keydown', this.handleKeyDown)

    // Mount if container provided
    if (config.container !== undefined) {
      const parent = resolveContainer(config.container)
      parent.appendChild(this.el)
    }
  }

  /** Get the current value */
  getValue(): number {
    return this.currentValue
  }

  /** Set the current value */
  setValue(value: number): void {
    const clamped = this.clamp(value)
    if (clamped !== this.currentValue) {
      this.currentValue = clamped
      this.updatePosition()
      this.onChangeCallback?.(this.currentValue)
    }
  }

  /** Set the disabled state */
  setDisabled(disabled: boolean): void {
    if (disabled) {
      this.el.classList.add('cmm-slider--disabled')
      this.thumbEl.setAttribute('aria-disabled', 'true')
      this.thumbEl.tabIndex = -1
    } else {
      this.el.classList.remove('cmm-slider--disabled')
      this.thumbEl.removeAttribute('aria-disabled')
      this.thumbEl.tabIndex = 0
    }
  }

  /** Update the range */
  setRange(min: number, max: number): void {
    this.min = min
    this.max = max > min ? max : min + 1
    this.currentValue = this.clamp(this.currentValue)
    this.thumbEl.setAttribute('aria-valuemin', String(this.min))
    this.thumbEl.setAttribute('aria-valuemax', String(this.max))
    this.updatePosition()

    if (this.showTicks) {
      this.buildTicks()
    }
  }

  /** Remove from DOM and clean up */
  destroy(): void {
    // Remove document-level listeners if still dragging
    document.removeEventListener('pointermove', this.handlePointerMove)
    document.removeEventListener('pointerup', this.handlePointerUp)

    this.thumbEl.removeEventListener('pointerdown', this.handlePointerDown)
    this.trackEl.removeEventListener('pointerdown', this.handleTrackClick)
    this.thumbEl.removeEventListener('keydown', this.handleKeyDown)
    this.el.remove()
  }

  private buildClassName(extra?: string): string {
    const classes = ['cmm-slider']
    if (extra) {
      classes.push(extra)
    }
    return classes.join(' ')
  }

  private getPercent(): number {
    const range = this.max - this.min
    if (range <= 0) {
      return 0
    }
    return ((this.currentValue - this.min) / range) * 100
  }

  private updatePosition(): void {
    const percent = this.getPercent()
    this.fillEl.style.width = `${String(percent)}%`
    this.thumbEl.style.left = `${String(percent)}%`
    this.thumbEl.setAttribute('aria-valuenow', String(this.currentValue))

    if (this.tooltipEl !== null) {
      this.tooltipEl.textContent = String(this.currentValue)
    }
  }

  private clamp(value: number): number {
    // Snap to nearest step
    const steppedValue = Math.round((value - this.min) / this.step) * this.step + this.min
    return Math.min(Math.max(steppedValue, this.min), this.max)
  }

  private getValueFromPointer(e: PointerEvent): number {
    const rect = this.trackEl.getBoundingClientRect()
    const x = e.clientX - rect.left
    const ratio = Math.min(Math.max(x / rect.width, 0), 1)
    return this.clamp(this.min + ratio * (this.max - this.min))
  }

  private onPointerDown(e: PointerEvent): void {
    e.preventDefault()
    e.stopPropagation()
    this.isDragging = true
    this.thumbEl.setPointerCapture(e.pointerId)
    document.addEventListener('pointermove', this.handlePointerMove)
    document.addEventListener('pointerup', this.handlePointerUp)

    if (this.tooltipEl !== null) {
      this.tooltipEl.classList.add('cmm-slider__tooltip--visible')
    }
  }

  private onPointerMove(e: PointerEvent): void {
    if (!this.isDragging) {
      return
    }
    const newValue = this.getValueFromPointer(e)
    if (newValue !== this.currentValue) {
      this.currentValue = newValue
      this.updatePosition()
      this.onChangeCallback?.(this.currentValue)
    }
  }

  private onPointerUp(_e: PointerEvent): void {
    if (!this.isDragging) {
      return
    }
    this.isDragging = false
    document.removeEventListener('pointermove', this.handlePointerMove)
    document.removeEventListener('pointerup', this.handlePointerUp)

    if (this.tooltipEl !== null) {
      this.tooltipEl.classList.remove('cmm-slider__tooltip--visible')
    }

    this.onChangeEndCallback?.(this.currentValue)
  }

  private onTrackClick(e: PointerEvent): void {
    // Ignore clicks on the thumb itself
    if (e.target === this.thumbEl) {
      return
    }
    const newValue = this.getValueFromPointer(e)
    if (newValue !== this.currentValue) {
      this.currentValue = newValue
      this.updatePosition()
      this.onChangeCallback?.(this.currentValue)
      this.onChangeEndCallback?.(this.currentValue)
    }
  }

  private onKeyDown(e: KeyboardEvent): void {
    let newValue: number

    switch (e.key) {
      case 'ArrowRight':
      case 'ArrowUp':
        newValue = this.clamp(this.currentValue + this.step)
        break
      case 'ArrowLeft':
      case 'ArrowDown':
        newValue = this.clamp(this.currentValue - this.step)
        break
      case 'Home':
        newValue = this.min
        break
      case 'End':
        newValue = this.max
        break
      default:
        return
    }

    e.preventDefault()

    if (newValue !== this.currentValue) {
      this.currentValue = newValue
      this.updatePosition()
      this.onChangeCallback?.(this.currentValue)
      this.onChangeEndCallback?.(this.currentValue)
    }
  }

  private buildTicks(): void {
    // Remove existing ticks
    if (this.ticksEl !== null) {
      this.ticksEl.remove()
    }

    this.ticksEl = document.createElement('div')
    this.ticksEl.className = 'cmm-slider__ticks'

    const range = this.max - this.min
    if (range <= 0) {
      return
    }

    const numSteps = Math.floor(range / this.step)
    for (let i = 0; i <= numSteps; i++) {
      const tick = document.createElement('div')
      tick.className = 'cmm-slider__tick'
      const percent = (i / numSteps) * 100
      tick.style.left = `${String(percent)}%`
      this.ticksEl.appendChild(tick)
    }

    this.el.appendChild(this.ticksEl)
  }
}
