/**
 * Radar (spider) chart using Canvas 2D
 * @countermeasure/web-components/charts/radar
 */

import { InteractiveChart } from '../interactive'
import type { LegendItem } from '../types'
import { defaultColors, hexToRgba, formatNumber } from '../utils'
import type { RadarChartConfig, RadarDataset } from './types'

const TWO_PI = Math.PI * 2
const GRID_COLOR = '#e2e8f0'
const SPOKE_COLOR = '#cbd5e1'
const LABEL_COLOR = '#334155'
const LABEL_FONT = '11px system-ui, sans-serif'
const DOT_RADIUS = 3.5
const LABEL_OFFSET = 14

export class RadarChart extends InteractiveChart {
  private labels: string[]
  private datasets: RadarDataset[]
  private colors: string[]
  private levels: number
  private showDots: boolean
  private fillOpacity: number
  private maxValue: number | undefined

  private activeDatasets: boolean[]
  private hoveredDataset = -1
  private hoveredAxis = -1

  constructor(config: RadarChartConfig) {
    super(config)
    this.labels = config.labels
    this.datasets = config.datasets
    this.colors = defaultColors()
    this.levels = config.levels ?? 5
    this.showDots = config.showDots ?? true
    this.fillOpacity = config.fillOpacity ?? 0.2
    this.maxValue = config.max
    this.activeDatasets = config.datasets.map(() => true)

    if (this.legend !== null) {
      this.legend.onToggle((index, active) => {
        if (index < this.activeDatasets.length) {
          this.activeDatasets[index] = active
        }
        this.render()
      })
    }

    this.render()
  }

  /** Replace datasets and re-render */
  update(datasets: RadarDataset[]): void {
    this.datasets = datasets
    this.activeDatasets = datasets.map(() => true)
    this.render()
  }

  render(): void {
    this.clear()

    const plot = this.plotArea
    const cx = plot.x + plot.width / 2
    const cy = plot.y + plot.height / 2
    const radius = Math.min(plot.width, plot.height) / 2 - LABEL_OFFSET - 4
    const axisCount = this.labels.length
    if (axisCount < 3) return

    const computedMax = this.computeMax()
    if (computedMax <= 0) return

    const angleStep = TWO_PI / axisCount

    // Draw concentric rings
    this.drawLevels(cx, cy, radius, axisCount, angleStep)

    // Draw spokes
    this.drawSpokes(cx, cy, radius, axisCount, angleStep)

    // Draw axis labels
    this.drawLabels(cx, cy, radius, axisCount, angleStep)

    // Draw datasets
    for (let di = 0; di < this.datasets.length; di++) {
      const ds = this.datasets[di]
      if (ds === undefined) continue
      const isActive = this.activeDatasets[di] ?? true
      if (!isActive) continue

      const color = ds.color ?? this.colors[di % this.colors.length] ?? '#3b82f6'
      const opacity = ds.fillOpacity ?? this.fillOpacity
      const isHovered = di === this.hoveredDataset

      this.drawDataset(
        cx,
        cy,
        radius,
        computedMax,
        axisCount,
        angleStep,
        ds,
        color,
        opacity,
        isHovered
      )
    }

    this.updateLegend()
  }

  private computeMax(): number {
    if (this.maxValue !== undefined) return this.maxValue

    let max = 0
    for (let di = 0; di < this.datasets.length; di++) {
      const ds = this.datasets[di]
      if (ds === undefined) continue
      const isActive = this.activeDatasets[di] ?? true
      if (!isActive) continue

      for (const v of ds.values) {
        if (v > max) {
          max = v
        }
      }
    }
    return max > 0 ? max : 1
  }

  private drawLevels(
    cx: number,
    cy: number,
    radius: number,
    axisCount: number,
    angleStep: number
  ): void {
    this.ctx.save()
    this.ctx.strokeStyle = GRID_COLOR
    this.ctx.lineWidth = 0.75

    for (let level = 1; level <= this.levels; level++) {
      const r = (radius / this.levels) * level

      this.ctx.beginPath()
      for (let i = 0; i <= axisCount; i++) {
        const angle = i * angleStep - Math.PI / 2
        const x = cx + Math.cos(angle) * r
        const y = cy + Math.sin(angle) * r
        if (i === 0) {
          this.ctx.moveTo(x, y)
        } else {
          this.ctx.lineTo(x, y)
        }
      }
      this.ctx.closePath()
      this.ctx.stroke()
    }

    this.ctx.restore()
  }

  private drawSpokes(
    cx: number,
    cy: number,
    radius: number,
    axisCount: number,
    angleStep: number
  ): void {
    this.ctx.save()
    this.ctx.strokeStyle = SPOKE_COLOR
    this.ctx.lineWidth = 0.75

    for (let i = 0; i < axisCount; i++) {
      const angle = i * angleStep - Math.PI / 2
      this.ctx.beginPath()
      this.ctx.moveTo(cx, cy)
      this.ctx.lineTo(cx + Math.cos(angle) * radius, cy + Math.sin(angle) * radius)
      this.ctx.stroke()
    }

    this.ctx.restore()
  }

  private drawLabels(
    cx: number,
    cy: number,
    radius: number,
    axisCount: number,
    angleStep: number
  ): void {
    this.ctx.save()
    this.ctx.fillStyle = LABEL_COLOR
    this.ctx.font = LABEL_FONT
    this.ctx.textBaseline = 'middle'

    for (let i = 0; i < axisCount; i++) {
      const label = this.labels[i]
      if (label === undefined) continue

      const angle = i * angleStep - Math.PI / 2
      const lx = cx + Math.cos(angle) * (radius + LABEL_OFFSET)
      const ly = cy + Math.sin(angle) * (radius + LABEL_OFFSET)

      // Align based on position
      const cos = Math.cos(angle)
      if (Math.abs(cos) < 0.01) {
        this.ctx.textAlign = 'center'
      } else if (cos > 0) {
        this.ctx.textAlign = 'left'
      } else {
        this.ctx.textAlign = 'right'
      }

      this.ctx.fillText(label, lx, ly)
    }

    this.ctx.restore()
  }

  private drawDataset(
    cx: number,
    cy: number,
    radius: number,
    maxVal: number,
    axisCount: number,
    angleStep: number,
    ds: RadarDataset,
    color: string,
    opacity: number,
    isHovered: boolean
  ): void {
    const points: { x: number; y: number }[] = []

    for (let i = 0; i < axisCount; i++) {
      const val = ds.values[i] ?? 0
      const ratio = val / maxVal
      const angle = i * angleStep - Math.PI / 2
      const px = cx + Math.cos(angle) * radius * ratio
      const py = cy + Math.sin(angle) * radius * ratio
      points.push({ x: px, y: py })
    }

    if (points.length === 0) return

    // Fill polygon
    this.ctx.save()
    this.ctx.beginPath()
    const firstPoint = points[0]
    if (firstPoint !== undefined) {
      this.ctx.moveTo(firstPoint.x, firstPoint.y)
    }
    for (let i = 1; i < points.length; i++) {
      const p = points[i]
      if (p !== undefined) {
        this.ctx.lineTo(p.x, p.y)
      }
    }
    this.ctx.closePath()
    this.ctx.fillStyle = hexToRgba(color, isHovered ? opacity + 0.15 : opacity)
    this.ctx.fill()

    // Stroke outline
    this.ctx.strokeStyle = color
    this.ctx.lineWidth = isHovered ? 2.5 : 1.5
    this.ctx.stroke()
    this.ctx.restore()

    // Dots
    if (this.showDots) {
      this.ctx.save()
      for (const p of points) {
        this.ctx.beginPath()
        this.ctx.arc(p.x, p.y, DOT_RADIUS, 0, TWO_PI)
        this.ctx.fillStyle = color
        this.ctx.fill()
        this.ctx.strokeStyle = '#ffffff'
        this.ctx.lineWidth = 1
        this.ctx.stroke()
      }
      this.ctx.restore()
    }
  }

  private updateLegend(): void {
    if (this.legend === null) return
    const items: LegendItem[] = []
    for (let di = 0; di < this.datasets.length; di++) {
      const ds = this.datasets[di]
      if (ds === undefined) continue
      const color = ds.color ?? this.colors[di % this.colors.length] ?? '#3b82f6'
      const active = this.activeDatasets[di] ?? true
      items.push({ label: ds.label, color, active })
    }
    this.legend.update(items)
  }

  protected handleMouseMove(e: MouseEvent): void {
    const rect = this.canvas.getBoundingClientRect()
    const mx = e.clientX - rect.left
    const my = e.clientY - rect.top

    const plot = this.plotArea
    const cx = plot.x + plot.width / 2
    const cy = plot.y + plot.height / 2
    const radius = Math.min(plot.width, plot.height) / 2 - LABEL_OFFSET - 4
    const axisCount = this.labels.length
    if (axisCount < 3) return

    const computedMax = this.computeMax()
    if (computedMax <= 0) return

    const angleStep = TWO_PI / axisCount

    // Find closest axis
    let closestAxis = -1
    let closestDist = Infinity

    for (let i = 0; i < axisCount; i++) {
      const angle = i * angleStep - Math.PI / 2
      const axX = cx + Math.cos(angle) * radius
      const axY = cy + Math.sin(angle) * radius
      const dist = Math.sqrt((mx - axX) ** 2 + (my - axY) ** 2)
      if (dist < closestDist) {
        closestDist = dist
        closestAxis = i
      }
    }

    // Find closest dataset by checking distance to polygon points
    let closestDs = -1
    let dsMinDist = 30 // pixel threshold

    for (let di = 0; di < this.datasets.length; di++) {
      const ds = this.datasets[di]
      if (ds === undefined) continue
      const isActive = this.activeDatasets[di] ?? true
      if (!isActive) continue

      for (let i = 0; i < axisCount; i++) {
        const val = ds.values[i] ?? 0
        const ratio = val / computedMax
        const angle = i * angleStep - Math.PI / 2
        const px = cx + Math.cos(angle) * radius * ratio
        const py = cy + Math.sin(angle) * radius * ratio
        const dist = Math.sqrt((mx - px) ** 2 + (my - py) ** 2)
        if (dist < dsMinDist) {
          dsMinDist = dist
          closestDs = di
        }
      }
    }

    const needsRedraw = closestDs !== this.hoveredDataset || closestAxis !== this.hoveredAxis
    this.hoveredDataset = closestDs
    this.hoveredAxis = closestAxis

    if (needsRedraw) {
      this.render()
    }

    // Show tooltip
    if (closestDs !== -1 && closestAxis !== -1 && this.tooltip !== null) {
      const ds = this.datasets[closestDs]
      const label = this.labels[closestAxis]
      if (ds !== undefined && label !== undefined) {
        const val = ds.values[closestAxis] ?? 0
        const content = `<strong>${ds.label}</strong><br/>${label}: ${formatNumber(val)}`
        this.tooltip.show(mx, my, content)
      }
    } else if (this.tooltip !== null) {
      this.tooltip.hide()
    }
  }

  protected handleMouseLeave(): void {
    if (this.hoveredDataset !== -1 || this.hoveredAxis !== -1) {
      this.hoveredDataset = -1
      this.hoveredAxis = -1
      if (this.tooltip !== null) {
        this.tooltip.hide()
      }
      this.render()
    }
  }
}
