/**
 * DataTable - Accessible sortable, paginated data table component
 * @countermeasure/web-components/components/data-table
 *
 * Implements ARIA Grid pattern with full keyboard navigation.
 * WCAG 2.1 AA compliant.
 */

import { type ColumnDef, type DataTableConfig, type SortDirection, type SortState } from './types'
import { escapeHtml, renderCell } from './renderers'
import { copyToClipboard, exportCsv, exportJson } from './export'
import { uniqueId } from '../../primitives/utils'
import { announce, createLiveRegion, type LiveRegion } from '../../utils/a11y'
import { gridNavigate, Keys } from '../../utils/keyboard'
import { cycleSortDirection, sortRows } from '../../utils/sorting'

export type {
  CellRenderer,
  ColumnDef,
  DataTableConfig,
  PaginationConfig,
  SortDirection,
  SortState,
} from './types'

/** Resolve a container reference to an HTMLElement */
function resolveContainer(container: HTMLElement | string): HTMLElement {
  if (typeof container === 'string') {
    const el = document.querySelector<HTMLElement>(container)
    if (!el) {
      throw new Error(`DataTable container not found: ${container}`)
    }
    return el
  }
  return container
}

export class DataTable {
  private readonly container: HTMLElement
  private readonly columns: ColumnDef[]
  private readonly config: DataTableConfig
  private rows: unknown[][] = []
  private sortState: SortState | null = null
  private currentPage = 0
  private wrapperEl: HTMLElement | null = null
  private tableEl: HTMLTableElement | null = null
  private liveRegion: LiveRegion | null = null

  // Accessibility state
  private readonly tableId: string
  private readonly captionId: string
  private focusedRow = 0
  private focusedCol = 0

  constructor(config: DataTableConfig) {
    this.container = resolveContainer(config.container)
    this.columns = config.columns
    this.config = config
    this.tableId = uniqueId('cmm-data-table')
    this.captionId = uniqueId('cmm-data-table-caption')

    if (config.data !== undefined) {
      this.rows = config.data
    }

    // Create live region for announcements
    this.liveRegion = createLiveRegion('polite')

    this.render()
  }

  /** Replace all table data and re-render */
  setData(rows: unknown[][]): void {
    this.rows = rows
    this.currentPage = 0
    this.focusedRow = 0
    this.focusedCol = 0
    this.render()
    this.announceUpdate(`Table updated with ${String(rows.length)} rows`)
  }

  /** Append rows to existing data and re-render */
  appendRows(rows: unknown[][]): void {
    this.rows = [...this.rows, ...rows]
    this.render()
    this.announceUpdate(`Added ${String(rows.length)} rows, total ${String(this.rows.length)} rows`)
  }

  /** Show or hide a loading overlay on the table */
  setLoading(loading: boolean): void {
    if (!this.wrapperEl) {
      return
    }

    const existing = this.wrapperEl.querySelector('.cmm-data-table__loading')
    if (loading) {
      if (!existing) {
        const overlay = document.createElement('div')
        overlay.className = 'cmm-data-table__loading'
        overlay.setAttribute('role', 'status')
        overlay.setAttribute('aria-live', 'polite')

        const loadingText = document.createElement('span')
        loadingText.textContent = 'Loading...'
        overlay.appendChild(loadingText)

        this.wrapperEl.appendChild(overlay)
        this.announceUpdate('Loading data')
      }
    } else {
      if (existing) {
        existing.remove()
        this.announceUpdate('Data loaded')
      }
    }
  }

  /** Clear all data from the table */
  clear(): void {
    this.rows = []
    this.currentPage = 0
    this.focusedRow = 0
    this.focusedCol = 0
    this.render()
    this.announceUpdate('Table cleared')
  }

  /** Get the current number of rows */
  getRowCount(): number {
    return this.rows.length
  }

  /** Remove the table from the DOM and clean up */
  destroy(): void {
    this.container.innerHTML = ''
    this.wrapperEl = null
    this.tableEl = null
    this.liveRegion?.destroy()
    this.liveRegion = null
  }

  /** Announce an update to screen readers */
  private announceUpdate(message: string): void {
    this.liveRegion?.announce(message)
  }

  /** Get the currently visible page of rows */
  private getPageRows(): unknown[][] {
    const pagination = this.config.pagination
    if (pagination === undefined) {
      return this.rows
    }

    const start = this.currentPage * pagination.pageSize
    const end = start + pagination.pageSize
    return this.rows.slice(start, end)
  }

  /** Get total page count */
  private getTotalPages(): number {
    const pagination = this.config.pagination
    if (pagination === undefined) {
      return 1
    }
    return Math.max(1, Math.ceil(this.rows.length / pagination.pageSize))
  }

  /** Handle column header click for sorting */
  private handleSort(key: string): void {
    const col = this.columns.find(c => c.key === key)
    if (col?.sortable !== true) {
      return
    }

    const nextSort = cycleSortDirection(
      this.sortState?.key ?? null,
      this.sortState?.direction ?? null,
      key
    )

    this.sortState = nextSort
    const direction: SortDirection = nextSort?.direction ?? null

    if (direction === null) {
      this.announceUpdate(`${col.label} sort cleared`)
    } else {
      const directionLabel = direction === 'asc' ? 'ascending' : 'descending'
      this.announceUpdate(`Sorted by ${col.label} ${directionLabel}`)
    }

    if (this.config.onSort !== undefined) {
      this.config.onSort(key, direction)
    }

    this.currentPage = 0
    this.render()
  }

  /** Get sorted rows based on current sort state */
  private getSortedRows(): unknown[][] {
    if (this.sortState === null) {
      return [...this.rows]
    }

    const { key, direction } = this.sortState
    const colIndex = this.columns.findIndex(c => c.key === key)
    if (colIndex < 0 || direction === null) {
      return [...this.rows]
    }

    return sortRows(this.rows, direction, row => row[colIndex])
  }

  /** Build the sort icon HTML for a column header */
  private buildSortIcon(key: string): string {
    const sortedClass = this.sortState?.key === key ? 'cmm-data-table__sort-icon--active' : ''
    if (this.sortState?.key !== key) {
      return `<span class="cmm-data-table__sort-icon ${sortedClass}" aria-hidden="true">&#8597;</span>`
    }
    if (this.sortState.direction === 'asc') {
      return `<span class="cmm-data-table__sort-icon ${sortedClass}" aria-hidden="true">&#8593;</span>`
    }
    return `<span class="cmm-data-table__sort-icon ${sortedClass}" aria-hidden="true">&#8595;</span>`
  }

  /** Get the sort direction label for ARIA */
  private getSortLabel(key: string): 'ascending' | 'descending' | 'none' {
    if (this.sortState?.key !== key) {
      return 'none'
    }
    return this.sortState.direction === 'asc' ? 'ascending' : 'descending'
  }

  /** Handle keyboard navigation within the table */
  private handleKeyDown(event: KeyboardEvent): void {
    const target = event.target as HTMLElement
    if (this.tableEl?.contains(target) !== true) {
      return
    }

    // Get all navigable rows (data rows only, not header)
    const tbody = this.tableEl.querySelector('tbody')
    if (!tbody) {
      return
    }

    const rows = Array.from(tbody.querySelectorAll<HTMLElement>('tr'))
    const pageRows = this.getPageRows()

    if (pageRows.length === 0) {
      return
    }

    const result = gridNavigate(event, {
      columns: this.columns.length,
      rows: pageRows.length,
      currentCol: this.focusedCol,
      currentRow: this.focusedRow,
      wrap: false,
    })

    if (result.handled) {
      event.preventDefault()
      this.focusedRow = result.row
      this.focusedCol = result.col
      this.focusCell(rows)
    }

    // Handle Enter/Space for row activation
    if (event.key === Keys.ENTER || event.key === Keys.SPACE) {
      const pagination = this.config.pagination
      const startIndex = pagination !== undefined ? this.currentPage * pagination.pageSize : 0
      const absoluteIndex = startIndex + this.focusedRow
      const row = this.rows[absoluteIndex]

      if (row !== undefined && this.config.onRowClick !== undefined) {
        event.preventDefault()
        this.config.onRowClick(row, absoluteIndex)
      }
    }
  }

  /** Focus a specific cell */
  private focusCell(rows: HTMLElement[]): void {
    const row = rows[this.focusedRow]
    if (row === undefined) {
      return
    }

    const cells = Array.from(row.querySelectorAll<HTMLElement>('td'))
    const cell = cells[this.focusedCol]
    if (cell !== undefined) {
      cell.focus()

      // Announce cell content
      const cellContent = cell.textContent
      const col = this.columns[this.focusedCol]
      const columnLabel = col?.label ?? ''
      announce(`${columnLabel}: ${cellContent}`, 'polite')
    }
  }

  /** Render the full table into the container */
  private render(): void {
    this.container.innerHTML = ''

    const wrapper = document.createElement('div')
    wrapper.className = 'cmm-data-table'
    this.wrapperEl = wrapper

    const pagination = this.config.pagination

    // Top pagination
    if (
      pagination !== undefined &&
      (pagination.position === 'top' || pagination.position === 'both')
    ) {
      wrapper.appendChild(this.buildPagination('top'))
    }

    // Export buttons
    if (this.config.exportFormats !== undefined && this.config.exportFormats.length > 0) {
      wrapper.appendChild(this.buildExportBar())
    }

    // Sort rows, then paginate
    const allSorted = this.getSortedRows()
    // Temporarily swap this.rows for pagination
    const originalRows = this.rows
    this.rows = allSorted

    const pageRows = this.getPageRows()
    this.rows = originalRows

    // Table with ARIA grid pattern
    const table = document.createElement('table')
    table.className = 'cmm-data-table__table'
    table.id = this.tableId
    table.setAttribute('role', 'grid')
    table.setAttribute('aria-labelledby', this.captionId)
    table.setAttribute('aria-rowcount', String(this.rows.length))
    table.setAttribute('aria-colcount', String(this.columns.length))
    this.tableEl = table

    // Caption (visually hidden but accessible)
    const caption = document.createElement('caption')
    caption.id = this.captionId
    caption.className = 'cmm-sr-only'
    caption.textContent =
      this.config.emptyMessage !== undefined
        ? `Data table. ${String(this.rows.length)} rows, ${String(this.columns.length)} columns. Use arrow keys to navigate.`
        : `Data table with ${String(this.rows.length)} rows and ${String(this.columns.length)} columns. Use arrow keys to navigate.`
    table.appendChild(caption)

    // Header
    const thead = document.createElement('thead')
    thead.className = 'cmm-data-table__head'
    const headerRow = document.createElement('tr')
    headerRow.setAttribute('role', 'row')

    for (let colIdx = 0; colIdx < this.columns.length; colIdx++) {
      const col = this.columns[colIdx]
      if (col === undefined) {
        continue
      }

      const th = document.createElement('th')
      th.className = 'cmm-data-table__header'
      th.setAttribute('role', 'columnheader')
      th.setAttribute('scope', 'col')
      th.setAttribute('aria-colindex', String(colIdx + 1))

      if (col.width !== undefined) {
        th.style.width = col.width
      }

      if (col.className !== undefined) {
        th.classList.add(col.className)
      }

      if (col.sortable === true) {
        th.classList.add('cmm-data-table__header--sortable')
        th.setAttribute('aria-sort', this.getSortLabel(col.key))
        th.setAttribute('tabindex', '0')
        th.innerHTML = `${escapeHtml(col.label)} ${this.buildSortIcon(col.key)}`

        th.addEventListener('click', () => {
          this.handleSort(col.key)
        })

        th.addEventListener('keydown', (e: KeyboardEvent) => {
          if (e.key === Keys.ENTER || e.key === Keys.SPACE) {
            e.preventDefault()
            this.handleSort(col.key)
          }
        })
      } else {
        th.textContent = col.label
      }

      if (this.sortState !== null && this.sortState.key === col.key) {
        th.classList.add('cmm-data-table__cell--sorted')
      }

      headerRow.appendChild(th)
    }

    thead.appendChild(headerRow)
    table.appendChild(thead)

    // Body
    const tbody = document.createElement('tbody')
    tbody.className = 'cmm-data-table__body'

    if (pageRows.length === 0) {
      const emptyRow = document.createElement('tr')
      emptyRow.setAttribute('role', 'row')

      const emptyCell = document.createElement('td')
      emptyCell.className = 'cmm-data-table__empty'
      emptyCell.colSpan = this.columns.length
      emptyCell.setAttribute('role', 'gridcell')
      emptyCell.textContent = this.config.emptyMessage ?? 'No data available'
      emptyRow.appendChild(emptyCell)
      tbody.appendChild(emptyRow)
    } else {
      // Compute the absolute start index for click handlers
      const startIndex = pagination !== undefined ? this.currentPage * pagination.pageSize : 0

      for (let rowIdx = 0; rowIdx < pageRows.length; rowIdx++) {
        const row = pageRows[rowIdx]
        if (row === undefined) {
          continue
        }

        const tr = document.createElement('tr')
        tr.className = 'cmm-data-table__row'
        tr.setAttribute('role', 'row')
        tr.setAttribute('aria-rowindex', String(startIndex + rowIdx + 1))

        const absoluteIndex = startIndex + rowIdx

        if (this.config.onRowClick !== undefined) {
          tr.classList.add('cmm-data-table__row--clickable')
          tr.style.cursor = 'pointer'
          tr.addEventListener('click', () => {
            this.config.onRowClick?.(row, absoluteIndex)
          })
        }

        if (this.config.onRowContext !== undefined) {
          tr.addEventListener('contextmenu', (event: MouseEvent) => {
            this.config.onRowContext?.(row, absoluteIndex, event)
          })
        }

        for (let colIdx = 0; colIdx < this.columns.length; colIdx++) {
          const col = this.columns[colIdx]
          if (col === undefined) {
            continue
          }

          const td = document.createElement('td')
          td.className = 'cmm-data-table__cell'
          td.setAttribute('role', 'gridcell')
          td.setAttribute('aria-colindex', String(colIdx + 1))
          td.setAttribute(
            'tabindex',
            rowIdx === this.focusedRow && colIdx === this.focusedCol ? '0' : '-1'
          )

          if (col.className !== undefined) {
            td.classList.add(col.className)
          }

          if (this.sortState !== null && this.sortState.key === col.key) {
            td.classList.add('cmm-data-table__cell--sorted')
          }

          const cellValue = row[colIdx]
          const renderer = col.renderer ?? 'text'
          const rendererOptions: Record<string, unknown> = {}

          if (col.badgeMap !== undefined) {
            rendererOptions.badgeMap = col.badgeMap
          }
          if (col.format !== undefined) {
            rendererOptions.format = col.format
          }

          td.innerHTML = renderCell(cellValue, renderer, rendererOptions)
          tr.appendChild(td)
        }

        tbody.appendChild(tr)
      }
    }

    table.appendChild(tbody)

    // Add keyboard navigation handler
    table.addEventListener('keydown', e => {
      this.handleKeyDown(e)
    })

    wrapper.appendChild(table)

    // Bottom pagination
    if (
      pagination !== undefined &&
      (pagination.position === undefined ||
        pagination.position === 'bottom' ||
        pagination.position === 'both')
    ) {
      wrapper.appendChild(this.buildPagination('bottom'))
    }

    this.container.appendChild(wrapper)
  }

  /** Build the pagination controls element */
  private buildPagination(position: 'top' | 'bottom'): HTMLElement {
    const nav = document.createElement('nav')
    nav.className = 'cmm-data-table__pagination'
    nav.setAttribute('role', 'navigation')
    nav.setAttribute('aria-label', `Table pagination ${position}`)

    const totalPages = this.getTotalPages()
    const info = document.createElement('span')
    info.className = 'cmm-data-table__pagination-info'
    info.setAttribute('aria-live', 'polite')
    info.textContent = `Page ${String(this.currentPage + 1)} of ${String(totalPages)}`
    nav.appendChild(info)

    const prevBtn = document.createElement('button')
    prevBtn.className = 'cmm-data-table__pagination-btn'
    prevBtn.type = 'button'
    prevBtn.textContent = 'Previous'
    prevBtn.disabled = this.currentPage === 0
    prevBtn.setAttribute('aria-label', 'Go to previous page')

    if (this.currentPage === 0) {
      prevBtn.setAttribute('aria-disabled', 'true')
    }

    prevBtn.addEventListener('click', () => {
      if (this.currentPage > 0) {
        this.currentPage--
        this.focusedRow = 0
        this.focusedCol = 0
        this.render()
        this.announceUpdate(`Page ${String(this.currentPage + 1)} of ${String(totalPages)}`)
      }
    })
    nav.appendChild(prevBtn)

    const nextBtn = document.createElement('button')
    nextBtn.className = 'cmm-data-table__pagination-btn'
    nextBtn.type = 'button'
    nextBtn.textContent = 'Next'
    nextBtn.disabled = this.currentPage >= totalPages - 1
    nextBtn.setAttribute('aria-label', 'Go to next page')

    if (this.currentPage >= totalPages - 1) {
      nextBtn.setAttribute('aria-disabled', 'true')
    }

    nextBtn.addEventListener('click', () => {
      if (this.currentPage < totalPages - 1) {
        this.currentPage++
        this.focusedRow = 0
        this.focusedCol = 0
        this.render()
        this.announceUpdate(`Page ${String(this.currentPage + 1)} of ${String(totalPages)}`)
      }
    })
    nav.appendChild(nextBtn)

    return nav
  }

  /** Build the export buttons bar */
  private buildExportBar(): HTMLElement {
    const bar = document.createElement('div')
    bar.className = 'cmm-data-table__export'
    bar.setAttribute('role', 'toolbar')
    bar.setAttribute('aria-label', 'Export options')

    const formats = this.config.exportFormats ?? []

    for (const format of formats) {
      const btn = document.createElement('button')
      btn.className = 'cmm-data-table__export-btn'
      btn.type = 'button'

      switch (format) {
        case 'csv': {
          btn.textContent = 'Export CSV'
          btn.setAttribute('aria-label', 'Export data as CSV file')
          btn.addEventListener('click', () => {
            const csv = exportCsv(this.columns, this.rows)
            const blob = new Blob([csv], { type: 'text/csv' })
            const url = URL.createObjectURL(blob)
            const a = document.createElement('a')
            a.href = url
            a.download = 'export.csv'
            a.click()
            URL.revokeObjectURL(url)
            this.announceUpdate('CSV file downloaded')
          })
          break
        }
        case 'json': {
          btn.textContent = 'Export JSON'
          btn.setAttribute('aria-label', 'Export data as JSON file')
          btn.addEventListener('click', () => {
            const json = exportJson(this.columns, this.rows)
            const blob = new Blob([json], { type: 'application/json' })
            const url = URL.createObjectURL(blob)
            const a = document.createElement('a')
            a.href = url
            a.download = 'export.json'
            a.click()
            URL.revokeObjectURL(url)
            this.announceUpdate('JSON file downloaded')
          })
          break
        }
        case 'clipboard': {
          btn.textContent = 'Copy to Clipboard'
          btn.setAttribute('aria-label', 'Copy data to clipboard')
          btn.addEventListener('click', () => {
            const csv = exportCsv(this.columns, this.rows)
            void copyToClipboard(csv).then(() => {
              this.announceUpdate('Data copied to clipboard')
            })
          })
          break
        }
      }

      bar.appendChild(btn)
    }

    return bar
  }
}
