/**
 * Calendar - Month-view grid with single and range selection
 * @countermeasure/web-components/form/calendar
 */

import { type CalendarConfig } from './types'
import { resolveContainer, uniqueId } from '../../primitives/utils'
import {
  buildDayGrid,
  DAYS_MON,
  DAYS_SUN,
  isBetween,
  isDateDisabled as sharedIsDateDisabled,
  isSameDay,
  MONTHS,
  stripTime,
  type WeekStart,
} from '../../utils/date'

export type { CalendarConfig, WeekStart } from './types'

export class Calendar {
  public readonly el: HTMLElement

  private readonly gridId: string
  private readonly labelId: string
  private config: CalendarConfig
  private viewYear: number
  private viewMonth: number
  private selected: Date | null
  private rangeStart: Date | null
  private rangeEnd: Date | null
  private hoverDate: Date | null = null
  private focusedDay: number
  private isDestroyed = false

  private headerEl!: HTMLElement
  private gridEl!: HTMLElement

  private readonly handleKeydown: (e: KeyboardEvent) => void
  private readonly handleGridClick: (e: MouseEvent) => void
  private readonly handleGridMouseover: (e: MouseEvent) => void
  private readonly handleGridMouseleave: () => void

  constructor(config: CalendarConfig) {
    this.config = config
    this.gridId = uniqueId('cmm-calendar-grid')
    this.labelId = uniqueId('cmm-calendar-label')

    const now = new Date()
    const initial = config.value ?? config.rangeStart ?? now
    this.viewYear = initial.getFullYear()
    this.viewMonth = initial.getMonth()
    this.selected = config.value ? stripTime(config.value) : null
    this.rangeStart = config.rangeStart ? stripTime(config.rangeStart) : null
    this.rangeEnd = config.rangeEnd ? stripTime(config.rangeEnd) : null
    this.focusedDay = (this.selected ?? now).getDate()

    this.el = document.createElement('div')
    this.el.className = this.buildRootClass()
    this.el.setAttribute('role', 'group')

    this.buildHeader()
    this.buildGrid()
    this.renderGrid()

    this.handleKeydown = (e: KeyboardEvent) => {
      if (this.isDestroyed) return
      this.onKeydown(e)
    }

    this.handleGridClick = (e: MouseEvent) => {
      if (this.isDestroyed) return
      const target = e.target as HTMLElement
      const cell = target.closest<HTMLElement>('[data-date]')
      if (cell === null) return
      const dateStr = cell.getAttribute('data-date')
      if (dateStr === null || cell.getAttribute('aria-disabled') === 'true') return
      this.selectDate(new Date(dateStr))
    }

    this.handleGridMouseover = (e: MouseEvent) => {
      if (this.isDestroyed || this.config.mode !== 'range') return
      const target = e.target as HTMLElement
      const cell = target.closest<HTMLElement>('[data-date]')
      if (cell === null) return
      const dateStr = cell.getAttribute('data-date')
      if (dateStr !== null) {
        this.hoverDate = new Date(dateStr)
        this.updateRangeHighlight()
      }
    }

    this.handleGridMouseleave = () => {
      if (this.isDestroyed || this.config.mode !== 'range') return
      this.hoverDate = null
      this.updateRangeHighlight()
    }

    this.gridEl.addEventListener('click', this.handleGridClick)
    this.gridEl.addEventListener('mouseover', this.handleGridMouseover)
    this.gridEl.addEventListener('mouseleave', this.handleGridMouseleave)
    this.el.addEventListener('keydown', this.handleKeydown)

    const parent = resolveContainer(config.container)
    parent.appendChild(this.el)
  }

  getValue(): Date | null {
    return this.selected
  }

  setValue(date: Date | null): void {
    this.selected = date !== null ? stripTime(date) : null
    if (date !== null) {
      this.viewYear = date.getFullYear()
      this.viewMonth = date.getMonth()
      this.focusedDay = date.getDate()
    }
    this.renderGrid()
    this.updateHeaderLabel()
  }

  setRange(start: Date | null, end: Date | null): void {
    this.rangeStart = start !== null ? stripTime(start) : null
    this.rangeEnd = end !== null ? stripTime(end) : null
    this.renderGrid()
  }

  goToMonth(year: number, month: number): void {
    this.viewYear = year
    this.viewMonth = month
    this.focusedDay = 1
    this.renderGrid()
    this.updateHeaderLabel()
  }

  destroy(): void {
    this.isDestroyed = true
    this.gridEl.removeEventListener('click', this.handleGridClick)
    this.gridEl.removeEventListener('mouseover', this.handleGridMouseover)
    this.gridEl.removeEventListener('mouseleave', this.handleGridMouseleave)
    this.el.removeEventListener('keydown', this.handleKeydown)
    this.el.remove()
  }

  private buildRootClass(): string {
    const classes = ['cmm-calendar']
    if (this.config.className) classes.push(this.config.className)
    return classes.join(' ')
  }

  private buildHeader(): void {
    this.headerEl = document.createElement('div')
    this.headerEl.className = 'cmm-calendar__header'

    const prevBtn = document.createElement('button')
    prevBtn.type = 'button'
    prevBtn.className = 'cmm-calendar__nav-btn'
    prevBtn.setAttribute('aria-label', 'Previous month')
    prevBtn.innerHTML =
      '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M10 12L6 8L10 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>'
    prevBtn.addEventListener('click', () => {
      if (this.isDestroyed) return
      this.navigateMonth(-1)
    })

    const titleEl = document.createElement('span')
    titleEl.id = this.labelId
    titleEl.className = 'cmm-calendar__title'
    titleEl.textContent = `${MONTHS[this.viewMonth] ?? ''} ${String(this.viewYear)}`

    const nextBtn = document.createElement('button')
    nextBtn.type = 'button'
    nextBtn.className = 'cmm-calendar__nav-btn'
    nextBtn.setAttribute('aria-label', 'Next month')
    nextBtn.innerHTML =
      '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M6 4L10 8L6 12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>'
    nextBtn.addEventListener('click', () => {
      if (this.isDestroyed) return
      this.navigateMonth(1)
    })

    this.headerEl.appendChild(prevBtn)
    this.headerEl.appendChild(titleEl)
    this.headerEl.appendChild(nextBtn)
    this.el.appendChild(this.headerEl)
  }

  private buildGrid(): void {
    this.gridEl = document.createElement('div')
    this.gridEl.id = this.gridId
    this.gridEl.className = 'cmm-calendar__grid'
    this.gridEl.setAttribute('role', 'grid')
    this.gridEl.setAttribute('aria-labelledby', this.labelId)
    this.gridEl.tabIndex = 0
    this.el.appendChild(this.gridEl)
  }

  private renderGrid(): void {
    this.gridEl.innerHTML = ''

    const weekStart = this.config.weekStart ?? 0
    const dayLabels = weekStart === 1 ? DAYS_MON : DAYS_SUN

    const weekdayRow = document.createElement('div')
    weekdayRow.className = 'cmm-calendar__weekdays'
    weekdayRow.setAttribute('role', 'row')
    for (const label of dayLabels) {
      const cell = document.createElement('span')
      cell.className = 'cmm-calendar__weekday'
      cell.setAttribute('role', 'columnheader')
      cell.setAttribute('aria-label', label)
      cell.textContent = label
      weekdayRow.appendChild(cell)
    }
    this.gridEl.appendChild(weekdayRow)

    const days = this.buildDayGrid(weekStart)
    const today = stripTime(new Date())

    let row = document.createElement('div')
    row.className = 'cmm-calendar__row'
    row.setAttribute('role', 'row')

    for (let i = 0; i < days.length; i++) {
      if (i > 0 && i % 7 === 0) {
        this.gridEl.appendChild(row)
        row = document.createElement('div')
        row.className = 'cmm-calendar__row'
        row.setAttribute('role', 'row')
      }

      const day = days[i]
      if (day === undefined) continue
      const cell = document.createElement('button')
      cell.type = 'button'
      cell.setAttribute('role', 'gridcell')
      cell.setAttribute('data-date', day.date.toISOString())
      cell.textContent = String(day.date.getDate())

      const classes = ['cmm-calendar__day']
      const isOutside = day.date.getMonth() !== this.viewMonth
      const isDisabled = this.isDateDisabled(day.date)

      if (isOutside) {
        classes.push('cmm-calendar__day--outside')
        if (this.config.showOutsideDays === false) {
          cell.style.visibility = 'hidden'
        }
      }

      if (isDisabled) {
        classes.push('cmm-calendar__day--disabled')
        cell.setAttribute('aria-disabled', 'true')
        cell.tabIndex = -1
      }

      if (isSameDay(day.date, today)) {
        classes.push('cmm-calendar__day--today')
      }

      if (this.config.mode === 'range') {
        this.applyRangeClasses(day.date, classes)
      } else if (this.selected !== null && isSameDay(day.date, this.selected)) {
        classes.push('cmm-calendar__day--selected')
        cell.setAttribute('aria-selected', 'true')
      }

      if (!isOutside && !isDisabled && day.date.getDate() === this.focusedDay) {
        cell.tabIndex = 0
      } else if (!isDisabled) {
        cell.tabIndex = -1
      }

      cell.className = classes.join(' ')
      row.appendChild(cell)
    }
    this.gridEl.appendChild(row)
  }

  private buildDayGrid(weekStart: WeekStart): { date: Date }[] {
    return buildDayGrid(this.viewYear, this.viewMonth, weekStart).map(date => ({ date }))
  }

  private isDateDisabled(date: Date): boolean {
    return sharedIsDateDisabled(
      date,
      this.config.minDate,
      this.config.maxDate,
      this.config.disabledDates
    )
  }

  private applyRangeClasses(date: Date, classes: string[]): void {
    const start = this.rangeStart
    const end = this.rangeEnd ?? this.hoverDate

    if (start !== null && isSameDay(date, start)) {
      classes.push('cmm-calendar__day--range-start')
      classes.push('cmm-calendar__day--selected')
    }
    if (end !== null && isSameDay(date, end)) {
      classes.push('cmm-calendar__day--range-end')
      classes.push('cmm-calendar__day--selected')
    }
    if (start !== null && end !== null && isBetween(date, start, end)) {
      classes.push('cmm-calendar__day--in-range')
    }
  }

  private updateRangeHighlight(): void {
    const cells = this.gridEl.querySelectorAll<HTMLElement>('[data-date]')
    for (const cell of cells) {
      const dateStr = cell.getAttribute('data-date')
      if (dateStr === null) continue
      const date = new Date(dateStr)

      cell.classList.remove(
        'cmm-calendar__day--range-start',
        'cmm-calendar__day--range-end',
        'cmm-calendar__day--in-range',
        'cmm-calendar__day--selected'
      )

      if (
        this.selected !== null &&
        isSameDay(date, this.selected) &&
        this.config.mode !== 'range'
      ) {
        cell.classList.add('cmm-calendar__day--selected')
      }

      const start = this.rangeStart
      const end = this.rangeEnd ?? this.hoverDate
      if (start !== null && isSameDay(date, start)) {
        cell.classList.add('cmm-calendar__day--range-start', 'cmm-calendar__day--selected')
      }
      if (end !== null && isSameDay(date, end)) {
        cell.classList.add('cmm-calendar__day--range-end', 'cmm-calendar__day--selected')
      }
      if (start !== null && end !== null && isBetween(date, start, end)) {
        cell.classList.add('cmm-calendar__day--in-range')
      }
    }
  }

  private selectDate(date: Date): void {
    const d = stripTime(date)
    if (this.config.mode === 'range') {
      if (this.rangeStart === null || this.rangeEnd !== null) {
        this.rangeStart = d
        this.rangeEnd = null
      } else {
        if (d < this.rangeStart) {
          this.rangeEnd = this.rangeStart
          this.rangeStart = d
        } else {
          this.rangeEnd = d
        }
        this.config.onRangeChange?.(this.rangeStart, this.rangeEnd)
      }
      this.renderGrid()
      this.updateHeaderLabel()
    } else {
      this.selected = d
      this.focusedDay = d.getDate()
      this.renderGrid()
      this.config.onChange?.(d)
    }
  }

  private navigateMonth(delta: number): void {
    this.viewMonth += delta
    if (this.viewMonth > 11) {
      this.viewMonth = 0
      this.viewYear += 1
    } else if (this.viewMonth < 0) {
      this.viewMonth = 11
      this.viewYear -= 1
    }
    this.focusedDay = 1
    this.renderGrid()
    this.updateHeaderLabel()
  }

  private updateHeaderLabel(): void {
    const titleEl = this.headerEl.querySelector('.cmm-calendar__title')
    if (titleEl !== null) {
      titleEl.textContent = `${MONTHS[this.viewMonth] ?? ''} ${String(this.viewYear)}`
    }
  }

  private onKeydown(e: KeyboardEvent): void {
    const maxDay = new Date(this.viewYear, this.viewMonth + 1, 0).getDate()
    let handled = true

    switch (e.key) {
      case 'ArrowRight':
        this.focusedDay += 1
        if (this.focusedDay > maxDay) {
          this.navigateMonth(1)
          this.focusedDay = 1
        }
        break
      case 'ArrowLeft':
        this.focusedDay -= 1
        if (this.focusedDay < 1) {
          this.navigateMonth(-1)
          this.focusedDay = new Date(this.viewYear, this.viewMonth + 1, 0).getDate()
        }
        break
      case 'ArrowDown':
        this.focusedDay += 7
        if (this.focusedDay > maxDay) {
          this.focusedDay -= maxDay
          this.navigateMonth(1)
        }
        break
      case 'ArrowUp':
        this.focusedDay -= 7
        if (this.focusedDay < 1) {
          this.navigateMonth(-1)
          const prevMax = new Date(this.viewYear, this.viewMonth + 1, 0).getDate()
          this.focusedDay += prevMax
        }
        break
      case 'PageDown':
        this.navigateMonth(1)
        break
      case 'PageUp':
        this.navigateMonth(-1)
        break
      case 'Enter':
      case ' ': {
        const focusDate = new Date(this.viewYear, this.viewMonth, this.focusedDay)
        if (!this.isDateDisabled(focusDate)) {
          this.selectDate(focusDate)
        }
        break
      }
      default:
        handled = false
    }

    if (handled) {
      e.preventDefault()
      this.renderGrid()
      const active = this.gridEl.querySelector<HTMLElement>(
        `button[tabindex="0"]:not(.cmm-calendar__day--disabled)`
      )
      active?.focus()
    }
  }
}
