/**
 * Gauge chart using Canvas 2D
 * @countermeasure/web-components/charts/gauge
 */

import { BaseChart } from '../base'
import { defaultColors, clamp, formatNumber } from '../utils'
import type { GaugeChartConfig, GaugeThreshold } from './types'

const DEG_TO_RAD = Math.PI / 180
const BG_ARC_COLOR = '#e2e8f0'
const NEEDLE_COLOR = '#1e293b'
const VALUE_FONT = 'bold 28px system-ui, sans-serif'
const UNIT_FONT = '14px system-ui, sans-serif'
const LABEL_FONT = '13px system-ui, sans-serif'
const LABEL_COLOR = '#64748b'

export class GaugeChart extends BaseChart {
  private value: number
  private min: number
  private max: number
  private label: string | undefined
  private unit: string | undefined
  private thresholds: GaugeThreshold[]
  private arcWidth: number
  private startAngle: number
  private endAngle: number
  private showValue: boolean

  constructor(config: GaugeChartConfig) {
    super(config)
    this.value = config.value
    this.min = config.min ?? 0
    this.max = config.max ?? 100
    this.label = config.label
    this.unit = config.unit
    this.thresholds = config.thresholds ?? []
    this.arcWidth = config.arcWidth ?? 20
    this.startAngle = config.startAngle ?? 135
    this.endAngle = config.endAngle ?? 405
    this.showValue = config.showValue ?? true

    this.render()
  }

  /** Update just the current value and re-render */
  updateValue(value: number): void {
    this.value = value
    this.render()
  }

  /** Update the full configuration and re-render */
  updateConfig(config: Partial<GaugeChartConfig>): void {
    if (config.value !== undefined) {
      this.value = config.value
    }
    if (config.min !== undefined) {
      this.min = config.min
    }
    if (config.max !== undefined) {
      this.max = config.max
    }
    if (config.label !== undefined) {
      this.label = config.label
    }
    if (config.unit !== undefined) {
      this.unit = config.unit
    }
    if (config.thresholds !== undefined) {
      this.thresholds = config.thresholds
    }
    if (config.arcWidth !== undefined) {
      this.arcWidth = config.arcWidth
    }
    if (config.startAngle !== undefined) {
      this.startAngle = config.startAngle
    }
    if (config.endAngle !== undefined) {
      this.endAngle = config.endAngle
    }
    if (config.showValue !== undefined) {
      this.showValue = config.showValue
    }
    this.render()
  }

  render(): void {
    this.clear()

    const plot = this.plotArea
    const cx = plot.x + plot.width / 2
    const cy = plot.y + plot.height * 0.6
    const radius = Math.min(plot.width / 2, plot.height * 0.55) - this.arcWidth / 2

    const startRad = this.startAngle * DEG_TO_RAD
    const endRad = this.endAngle * DEG_TO_RAD
    const totalSweep = endRad - startRad

    // Draw background arc
    this.ctx.save()
    this.ctx.beginPath()
    this.ctx.arc(cx, cy, radius, startRad, endRad)
    this.ctx.strokeStyle = BG_ARC_COLOR
    this.ctx.lineWidth = this.arcWidth
    this.ctx.lineCap = 'round'
    this.ctx.stroke()
    this.ctx.restore()

    // Draw threshold segments or single fill arc
    if (this.thresholds.length > 0) {
      this.drawThresholdArcs(cx, cy, radius, startRad, totalSweep)
    } else {
      this.drawFillArc(cx, cy, radius, startRad, totalSweep)
    }

    // Draw needle
    this.drawNeedle(cx, cy, radius, startRad, totalSweep)

    // Draw center value
    if (this.showValue) {
      this.drawCenterText(cx, cy)
    }
  }

  override destroy(): void {
    super.destroy()
  }

  private drawThresholdArcs(
    cx: number,
    cy: number,
    radius: number,
    startRad: number,
    totalSweep: number
  ): void {
    // Sort thresholds by value
    const sorted = [...this.thresholds].sort((a, b) => a.value - b.value)
    const range = this.max - this.min
    if (range <= 0) return

    const clampedValue = clamp(this.value, this.min, this.max)
    const valueRatio = (clampedValue - this.min) / range

    let prevRatio = 0

    for (const threshold of sorted) {
      const thresholdRatio = clamp((threshold.value - this.min) / range, 0, 1)
      const segStart = startRad + prevRatio * totalSweep
      const segEnd = startRad + thresholdRatio * totalSweep

      // Only draw segments up to the current value
      const fillEnd = startRad + valueRatio * totalSweep
      const drawEnd = Math.min(segEnd, fillEnd)

      if (drawEnd > segStart) {
        this.ctx.save()
        this.ctx.beginPath()
        this.ctx.arc(cx, cy, radius, segStart, drawEnd)
        this.ctx.strokeStyle = threshold.color
        this.ctx.lineWidth = this.arcWidth
        this.ctx.lineCap = 'butt'
        this.ctx.stroke()
        this.ctx.restore()
      }

      prevRatio = thresholdRatio
    }

    // Fill remaining from last threshold to max (if value extends beyond last threshold)
    if (prevRatio < valueRatio) {
      const lastColor = sorted[sorted.length - 1]?.color ?? defaultColors()[0] ?? '#3b82f6'
      const segStart = startRad + prevRatio * totalSweep
      const fillEnd = startRad + valueRatio * totalSweep
      this.ctx.save()
      this.ctx.beginPath()
      this.ctx.arc(cx, cy, radius, segStart, fillEnd)
      this.ctx.strokeStyle = lastColor
      this.ctx.lineWidth = this.arcWidth
      this.ctx.lineCap = 'butt'
      this.ctx.stroke()
      this.ctx.restore()
    }
  }

  private drawFillArc(
    cx: number,
    cy: number,
    radius: number,
    startRad: number,
    totalSweep: number
  ): void {
    const range = this.max - this.min
    if (range <= 0) return

    const clampedValue = clamp(this.value, this.min, this.max)
    const ratio = (clampedValue - this.min) / range
    const fillEnd = startRad + ratio * totalSweep
    const color = defaultColors()[0] ?? '#3b82f6'

    this.ctx.save()
    this.ctx.beginPath()
    this.ctx.arc(cx, cy, radius, startRad, fillEnd)
    this.ctx.strokeStyle = color
    this.ctx.lineWidth = this.arcWidth
    this.ctx.lineCap = 'round'
    this.ctx.stroke()
    this.ctx.restore()
  }

  private drawNeedle(
    cx: number,
    cy: number,
    radius: number,
    startRad: number,
    totalSweep: number
  ): void {
    const range = this.max - this.min
    if (range <= 0) return

    const clampedValue = clamp(this.value, this.min, this.max)
    const ratio = (clampedValue - this.min) / range
    const needleAngle = startRad + ratio * totalSweep
    const needleLength = radius - this.arcWidth / 2 - 4

    this.ctx.save()

    // Needle line
    this.ctx.beginPath()
    this.ctx.moveTo(cx, cy)
    this.ctx.lineTo(
      cx + Math.cos(needleAngle) * needleLength,
      cy + Math.sin(needleAngle) * needleLength
    )
    this.ctx.strokeStyle = NEEDLE_COLOR
    this.ctx.lineWidth = 2.5
    this.ctx.lineCap = 'round'
    this.ctx.stroke()

    // Center dot
    this.ctx.beginPath()
    this.ctx.arc(cx, cy, 5, 0, Math.PI * 2)
    this.ctx.fillStyle = NEEDLE_COLOR
    this.ctx.fill()

    this.ctx.restore()
  }

  private drawCenterText(cx: number, cy: number): void {
    this.ctx.save()
    this.ctx.textAlign = 'center'
    this.ctx.textBaseline = 'middle'

    // Value text
    const valueText =
      this.unit !== undefined ? `${formatNumber(this.value)}${this.unit}` : formatNumber(this.value)

    this.ctx.fillStyle = NEEDLE_COLOR
    this.ctx.font = VALUE_FONT
    this.ctx.fillText(valueText, cx, cy + 30)

    // Optional label below value
    if (this.label !== undefined) {
      this.ctx.fillStyle = LABEL_COLOR
      this.ctx.font = LABEL_FONT
      this.ctx.fillText(this.label, cx, cy + 52)
    }

    // Min and max labels
    this.ctx.fillStyle = LABEL_COLOR
    this.ctx.font = UNIT_FONT

    const plot = this.plotArea
    const gaugeRadius = Math.min(plot.width / 2, plot.height * 0.55) - this.arcWidth / 2

    const startRad = this.startAngle * DEG_TO_RAD
    const endRad = this.endAngle * DEG_TO_RAD

    const minX = cx + Math.cos(startRad) * (gaugeRadius + this.arcWidth)
    const minY = cy + Math.sin(startRad) * (gaugeRadius + this.arcWidth)
    this.ctx.textAlign = 'center'
    this.ctx.fillText(formatNumber(this.min), minX, minY + 14)

    const maxX = cx + Math.cos(endRad) * (gaugeRadius + this.arcWidth)
    const maxY = cy + Math.sin(endRad) * (gaugeRadius + this.arcWidth)
    this.ctx.fillText(formatNumber(this.max), maxX, maxY + 14)

    this.ctx.restore()
  }
}
