/**
 * Abstract base chart class with accessibility support
 * @countermeasure/web-components/charts/base
 *
 * Provides ARIA support for canvas-based charts including:
 * - Accessible name and description
 * - Hidden data table for screen readers
 * - Keyboard navigation framework
 * WCAG 2.1 AA compliant.
 */

import { resolveContainer, uniqueId } from '../primitives/utils'
import type { BaseChartConfig, ChartPadding, PlotArea } from './types'
import { announce, createLiveRegion, type LiveRegion } from '../utils/a11y'
import { Keys } from '../utils/keyboard'

const DEFAULT_WIDTH = 600
const DEFAULT_HEIGHT = 400
const DEFAULT_PADDING: ChartPadding = {
  top: 20,
  right: 20,
  bottom: 40,
  left: 50,
}

/** Accessible data point for keyboard navigation */
export interface AccessibleDataPoint {
  /** Unique identifier for this point */
  id: string
  /** X coordinate for focus indicator */
  x: number
  /** Y coordinate for focus indicator */
  y: number
  /** Label for screen readers */
  label: string
  /** Value for screen readers */
  value: string | number
  /** Series name if applicable */
  series?: string
  /** Raw data point */
  data: unknown
}

export abstract class BaseChart {
  protected canvas: HTMLCanvasElement
  protected ctx: CanvasRenderingContext2D
  protected container: HTMLElement
  protected width: number
  protected height: number
  protected padding: ChartPadding
  protected responsive: boolean
  protected animate: boolean
  protected chartId: string
  protected destroyed: boolean
  protected liveRegion: LiveRegion | null = null

  private resizeObserver: ResizeObserver | null

  // Bound event handlers (stored for proper cleanup)
  private readonly boundHandleKeyDown: (e: KeyboardEvent) => void
  private readonly boundHandleFocus: () => void
  private readonly boundHandleBlur: () => void

  // Accessibility state
  protected readonly labelId: string
  protected readonly descId: string
  protected readonly tableId: string
  private wrapperEl: HTMLElement | null = null
  private accessiblePoints: AccessibleDataPoint[] = []
  private focusedPointIndex = -1
  private focusIndicator: HTMLElement | null = null
  private hiddenTable: HTMLElement | null = null

  // Subclasses should set these
  protected chartTitle = 'Chart'
  protected chartDescription = ''

  constructor(config: BaseChartConfig) {
    this.container = resolveContainer(config.container)
    this.width = config.width ?? DEFAULT_WIDTH
    this.height = config.height ?? DEFAULT_HEIGHT
    this.padding = {
      top: config.padding?.top ?? DEFAULT_PADDING.top,
      right: config.padding?.right ?? DEFAULT_PADDING.right,
      bottom: config.padding?.bottom ?? DEFAULT_PADDING.bottom,
      left: config.padding?.left ?? DEFAULT_PADDING.left,
    }
    this.responsive = config.responsive ?? false
    this.animate = config.animate ?? false
    this.destroyed = false

    // Respect reduced motion preference
    if (typeof window !== 'undefined' && this.animate) {
      const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
      if (prefersReducedMotion) {
        this.animate = false
      }
    }
    this.chartId = uniqueId('cmm-chart')
    this.labelId = uniqueId('cmm-chart-label')
    this.descId = uniqueId('cmm-chart-desc')
    this.tableId = uniqueId('cmm-chart-table')
    this.resizeObserver = null

    // Create wrapper for accessibility
    this.wrapperEl = document.createElement('div')
    this.wrapperEl.className = 'cmm-chart-wrapper'
    this.wrapperEl.style.position = 'relative'
    this.wrapperEl.style.display = 'inline-block'
    this.container.appendChild(this.wrapperEl)

    // Create canvas
    this.canvas = document.createElement('canvas')
    this.canvas.id = this.chartId
    this.canvas.width = this.width
    this.canvas.height = this.height

    // Canvas as image for screen readers
    this.canvas.setAttribute('role', 'img')
    this.canvas.setAttribute('aria-labelledby', this.labelId)
    this.canvas.setAttribute('aria-describedby', this.descId)
    this.canvas.setAttribute('tabindex', '0')

    this.wrapperEl.appendChild(this.canvas)

    const ctx = this.canvas.getContext('2d')
    if (ctx === null) {
      throw new Error('Failed to get 2D rendering context')
    }
    this.ctx = ctx

    this.setupCanvas()

    if (config.className !== undefined) {
      this.canvas.classList.add(config.className)
    }

    // Create accessible elements
    this.createAccessibleElements()

    // Create focus indicator
    this.createFocusIndicator()

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

    // Bind event handlers (store references for proper cleanup)
    this.boundHandleKeyDown = this.handleKeyDown.bind(this)
    this.boundHandleFocus = this.handleFocus.bind(this)
    this.boundHandleBlur = this.handleBlur.bind(this)

    // Add keyboard navigation
    this.canvas.addEventListener('keydown', this.boundHandleKeyDown)
    this.canvas.addEventListener('focus', this.boundHandleFocus)
    this.canvas.addEventListener('blur', this.boundHandleBlur)

    if (this.responsive) {
      this.resizeObserver = new ResizeObserver(entries => {
        const entry = entries[0]
        if (entry !== undefined) {
          const { width, height } = entry.contentRect
          this.resize(Math.round(width), Math.round(height))
        }
      })
      this.resizeObserver.observe(this.container)
    }
  }

  protected get plotArea(): PlotArea {
    return {
      x: this.padding.left,
      y: this.padding.top,
      width: this.width - this.padding.left - this.padding.right,
      height: this.height - this.padding.top - this.padding.bottom,
    }
  }

  protected clear(): void {
    this.ctx.clearRect(0, 0, this.width, this.height)
  }

  protected setupCanvas(): void {
    const dpr = window.devicePixelRatio !== 0 ? window.devicePixelRatio : 1
    this.canvas.width = this.width * dpr
    this.canvas.height = this.height * dpr
    this.canvas.style.width = `${String(this.width)}px`
    this.canvas.style.height = `${String(this.height)}px`
    this.ctx.setTransform(1, 0, 0, 1, 0, 0)
    this.ctx.scale(dpr, dpr)
  }

  abstract render(): void

  public resize(width?: number, height?: number): void {
    if (width !== undefined) {
      this.width = width
    }
    if (height !== undefined) {
      this.height = height
    }
    this.setupCanvas()
    this.render()
  }

  public toDataURL(type?: string): string {
    return this.canvas.toDataURL(type)
  }

  public destroy(): void {
    if (this.resizeObserver !== null) {
      this.resizeObserver.disconnect()
      this.resizeObserver = null
    }

    this.canvas.removeEventListener('keydown', this.boundHandleKeyDown)
    this.canvas.removeEventListener('focus', this.boundHandleFocus)
    this.canvas.removeEventListener('blur', this.boundHandleBlur)

    this.liveRegion?.destroy()
    this.liveRegion = null

    if (this.wrapperEl !== null) {
      this.wrapperEl.remove()
    }

    this.destroyed = true
  }

  // ============================================================================
  // Accessibility Methods
  // ============================================================================

  /** Create hidden accessible elements */
  private createAccessibleElements(): void {
    if (this.wrapperEl === null) {
      return
    }

    // Hidden label for chart title
    const label = document.createElement('span')
    label.id = this.labelId
    label.className = 'cmm-sr-only'
    label.style.cssText = `
      position: absolute;
      width: 1px;
      height: 1px;
      padding: 0;
      margin: -1px;
      overflow: hidden;
      clip: rect(0, 0, 0, 0);
      white-space: nowrap;
      border: 0;
    `
    this.wrapperEl.appendChild(label)

    // Hidden description
    const desc = document.createElement('span')
    desc.id = this.descId
    desc.className = 'cmm-sr-only'
    desc.style.cssText = label.style.cssText
    this.wrapperEl.appendChild(desc)

    // Hidden data table for screen readers
    const tableWrapper = document.createElement('div')
    tableWrapper.id = this.tableId
    tableWrapper.className = 'cmm-sr-only'
    tableWrapper.style.cssText = label.style.cssText
    this.hiddenTable = tableWrapper
    this.wrapperEl.appendChild(tableWrapper)
  }

  /** Create focus indicator overlay */
  private createFocusIndicator(): void {
    if (this.wrapperEl === null) {
      return
    }

    const indicator = document.createElement('div')
    indicator.className = 'cmm-chart-focus-indicator'
    indicator.style.cssText = `
      position: absolute;
      width: 12px;
      height: 12px;
      border: 2px solid hsl(var(--focus-ring, 189 94% 43%));
      border-radius: 50%;
      background: hsl(var(--focus-ring, 189 94% 43%) / 0.3);
      pointer-events: none;
      display: none;
      transform: translate(-50%, -50%);
      z-index: 10;
    `
    indicator.setAttribute('aria-hidden', 'true')
    this.focusIndicator = indicator
    this.wrapperEl.appendChild(indicator)
  }

  /** Update the accessible title and description */
  protected updateAccessibleInfo(): void {
    const label = document.getElementById(this.labelId)
    const desc = document.getElementById(this.descId)

    if (label !== null) {
      label.textContent = this.chartTitle
    }

    if (desc !== null) {
      const pointCount = this.accessiblePoints.length
      const baseDesc =
        this.chartDescription !== ''
          ? this.chartDescription
          : `Interactive chart with ${String(pointCount)} data points. Use arrow keys to navigate between data points.`
      desc.textContent = baseDesc
    }
  }

  /**
   * Register accessible data points for keyboard navigation.
   * Subclasses should call this after rendering with their data points.
   */
  protected setAccessiblePoints(points: AccessibleDataPoint[]): void {
    this.accessiblePoints = points
    this.focusedPointIndex = -1
    this.updateAccessibleInfo()
    this.updateHiddenTable()
  }

  /** Update the hidden data table for screen readers */
  private updateHiddenTable(): void {
    if (this.hiddenTable === null) {
      return
    }

    this.hiddenTable.innerHTML = ''

    if (this.accessiblePoints.length === 0) {
      return
    }

    const table = document.createElement('table')
    table.setAttribute('role', 'table')
    table.setAttribute('aria-label', `Data for ${this.chartTitle}`)

    // Determine if we have series
    const hasSeries = this.accessiblePoints.some(p => p.series !== undefined)

    // Header row
    const thead = document.createElement('thead')
    const headerRow = document.createElement('tr')

    if (hasSeries) {
      const seriesHeader = document.createElement('th')
      seriesHeader.setAttribute('scope', 'col')
      seriesHeader.textContent = 'Series'
      headerRow.appendChild(seriesHeader)
    }

    const labelHeader = document.createElement('th')
    labelHeader.setAttribute('scope', 'col')
    labelHeader.textContent = 'Label'
    headerRow.appendChild(labelHeader)

    const valueHeader = document.createElement('th')
    valueHeader.setAttribute('scope', 'col')
    valueHeader.textContent = 'Value'
    headerRow.appendChild(valueHeader)

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

    // Data rows
    const tbody = document.createElement('tbody')
    for (const point of this.accessiblePoints) {
      const row = document.createElement('tr')

      if (hasSeries) {
        const seriesCell = document.createElement('td')
        seriesCell.textContent = point.series ?? ''
        row.appendChild(seriesCell)
      }

      const labelCell = document.createElement('td')
      labelCell.textContent = point.label
      row.appendChild(labelCell)

      const valueCell = document.createElement('td')
      valueCell.textContent = String(point.value)
      row.appendChild(valueCell)

      tbody.appendChild(row)
    }
    table.appendChild(tbody)

    this.hiddenTable.appendChild(table)
  }

  /** Handle keyboard navigation */
  private handleKeyDown(event: KeyboardEvent): void {
    if (this.accessiblePoints.length === 0) {
      return
    }

    let handled = false
    let newIndex = this.focusedPointIndex

    switch (event.key) {
      case Keys.ARROW_RIGHT:
      case Keys.ARROW_DOWN:
        newIndex = this.focusedPointIndex + 1
        if (newIndex >= this.accessiblePoints.length) {
          newIndex = 0
        }
        handled = true
        break
      case Keys.ARROW_LEFT:
      case Keys.ARROW_UP:
        newIndex = this.focusedPointIndex - 1
        if (newIndex < 0) {
          newIndex = this.accessiblePoints.length - 1
        }
        handled = true
        break
      case Keys.HOME:
        newIndex = 0
        handled = true
        break
      case Keys.END:
        newIndex = this.accessiblePoints.length - 1
        handled = true
        break
      case Keys.ENTER:
      case Keys.SPACE:
        // Activate/select the current point
        if (this.focusedPointIndex >= 0) {
          const point = this.accessiblePoints[this.focusedPointIndex]
          if (point !== undefined) {
            this.onPointActivated(point)
          }
        }
        handled = true
        break
    }

    if (handled) {
      event.preventDefault()
      if (newIndex !== this.focusedPointIndex && newIndex >= 0) {
        this.focusedPointIndex = newIndex
        this.updateFocusIndicator()
        this.announcePoint()
      }
    }
  }

  /** Handle chart focus */
  private handleFocus(): void {
    if (this.accessiblePoints.length > 0 && this.focusedPointIndex < 0) {
      this.focusedPointIndex = 0
      this.updateFocusIndicator()
      this.announcePoint()
    }
  }

  /** Handle chart blur */
  private handleBlur(): void {
    this.hideFocusIndicator()
  }

  /** Update the focus indicator position */
  private updateFocusIndicator(): void {
    if (this.focusIndicator === null || this.focusedPointIndex < 0) {
      this.hideFocusIndicator()
      return
    }

    const point = this.accessiblePoints[this.focusedPointIndex]
    if (point === undefined) {
      this.hideFocusIndicator()
      return
    }

    this.focusIndicator.style.display = 'block'
    this.focusIndicator.style.left = `${String(point.x)}px`
    this.focusIndicator.style.top = `${String(point.y)}px`
  }

  /** Hide the focus indicator */
  private hideFocusIndicator(): void {
    if (this.focusIndicator !== null) {
      this.focusIndicator.style.display = 'none'
    }
  }

  /** Announce the current focused point */
  private announcePoint(): void {
    if (this.focusedPointIndex < 0) {
      return
    }

    const point = this.accessiblePoints[this.focusedPointIndex]
    if (point === undefined) {
      return
    }

    const position = `${String(this.focusedPointIndex + 1)} of ${String(this.accessiblePoints.length)}`
    let announcement = `${point.label}: ${String(point.value)}`

    if (point.series !== undefined) {
      announcement = `${point.series}, ${announcement}`
    }

    announcement += `. Point ${position}`

    announce(announcement, 'polite')
  }

  /**
   * Called when a data point is activated (Enter/Space pressed).
   * Subclasses can override to handle point selection.
   */
  protected onPointActivated(point: AccessibleDataPoint): void {
    announce(`${point.label} selected`, 'polite')
  }

  /**
   * Helper to generate accessible points from data.
   * Subclasses can use this as a template.
   */
  protected generateAccessiblePoint(
    id: string,
    x: number,
    y: number,
    label: string,
    value: string | number,
    data: unknown,
    series?: string
  ): AccessibleDataPoint {
    const point: AccessibleDataPoint = { id, x, y, label, value, data }
    if (series !== undefined) {
      point.series = series
    }
    return point
  }
}
