/**
 * Scatter chart component
 * @countermeasure/web-components/charts/scatter
 */

import { SeriesChart } from '../series'
import type { ChartSeries, DataPoint, ResolvedSeries } from '../types'
import { scaleLinear, hexToRgba, niceScale } from '../utils'
import { drawXAxis, drawYAxis } from '../axis'
import type { ScatterChartConfig } from './types'

const DEFAULT_DOT_RADIUS = 4
const HIGHLIGHT_RADIUS_BONUS = 2
const HOVER_THRESHOLD = 20

interface PlottedPoint {
  px: number
  py: number
  dataPoint: DataPoint
  seriesName: string
  color: string
}

interface TrendLine {
  slope: number
  intercept: number
}

export class ScatterChart extends SeriesChart {
  private readonly config: ScatterChartConfig
  private plottedPoints: PlottedPoint[]
  private highlightedPoint: PlottedPoint | null

  constructor(config: ScatterChartConfig) {
    super(config)
    this.config = config
    this.plottedPoints = []
    this.highlightedPoint = null
    this.render()
  }

  private computeDataBounds(visible: ResolvedSeries[]): {
    xMin: number
    xMax: number
    yMin: number
    yMax: number
    xLabels: string[]
  } {
    let xMin = Infinity
    let xMax = -Infinity
    let yMin = Infinity
    let yMax = -Infinity
    const xLabelsSet = new Set<string>()

    for (const s of visible) {
      for (const d of s.data) {
        const xVal = typeof d.x === 'number' ? d.x : parseFloat(d.x)
        if (!isNaN(xVal)) {
          if (xVal < xMin) xMin = xVal
          if (xVal > xMax) xMax = xVal
        }
        xLabelsSet.add(String(d.x))
        if (d.y < yMin) yMin = d.y
        if (d.y > yMax) yMax = d.y
      }
    }

    if (xMin === Infinity) {
      xMin = 0
      xMax = 1
    }
    if (yMin === Infinity) {
      yMin = 0
      yMax = 1
    }

    return {
      xMin: this.config.xAxis?.min ?? xMin,
      xMax: this.config.xAxis?.max ?? xMax,
      yMin: this.config.yAxis?.min ?? yMin,
      yMax: this.config.yAxis?.max ?? yMax,
      xLabels: Array.from(xLabelsSet),
    }
  }

  /** Compute linear regression (least squares) for a set of data points */
  private computeTrendLine(data: DataPoint[]): TrendLine | null {
    if (data.length < 2) return null

    const xValues: number[] = []
    const yValues: number[] = []

    for (const d of data) {
      const xVal = typeof d.x === 'number' ? d.x : parseFloat(d.x)
      if (!isNaN(xVal)) {
        xValues.push(xVal)
        yValues.push(d.y)
      }
    }

    const n = xValues.length
    if (n < 2) return null

    let sumX = 0
    let sumY = 0
    let sumXY = 0
    let sumXX = 0

    for (let i = 0; i < n; i++) {
      const xv = xValues[i]
      const yv = yValues[i]
      if (xv === undefined || yv === undefined) continue
      sumX += xv
      sumY += yv
      sumXY += xv * yv
      sumXX += xv * xv
    }

    const denominator = n * sumXX - sumX * sumX
    if (denominator === 0) return null

    const slope = (n * sumXY - sumX * sumY) / denominator
    const intercept = (sumY - slope * sumX) / n

    return { slope, intercept }
  }

  render(): void {
    this.clear()
    this.plottedPoints = []

    const visible = this.getVisibleSeries()
    const { xMin, xMax, yMin, yMax, xLabels } = this.computeDataBounds(visible)
    const plot = this.plotArea
    const xTickCount = this.config.xAxis?.tickCount ?? 5
    const yTickCount = this.config.yAxis?.tickCount ?? 5
    const niceX = niceScale(xMin, xMax, xTickCount)
    const niceY = niceScale(yMin, yMax, yTickCount)

    // Draw axes - use numeric labels for x-axis
    const xAxisLabels =
      xLabels.length > 0 ? xLabels : this.generateNumericLabels(niceX.min, niceX.max, niceX.step)
    drawXAxis(this.ctx, plot, xAxisLabels, this.config.xAxis)
    drawYAxis(this.ctx, plot, niceY.min, niceY.max, this.config.yAxis)

    // Create scales
    const xScale = scaleLinear([niceX.min, niceX.max], [plot.x, plot.x + plot.width])
    const yScale = scaleLinear([niceY.min, niceY.max], [plot.y + plot.height, plot.y])

    const dotRadius = this.config.dotRadius ?? DEFAULT_DOT_RADIUS
    const showTrendLine = this.config.showTrendLine === true

    // Draw trend lines first (behind dots)
    if (showTrendLine) {
      for (const s of visible) {
        const trend = this.computeTrendLine(s.data)
        if (trend !== null) {
          const x1 = niceX.min
          const y1 = trend.slope * x1 + trend.intercept
          const x2 = niceX.max
          const y2 = trend.slope * x2 + trend.intercept

          this.ctx.beginPath()
          this.ctx.moveTo(xScale(x1), yScale(y1))
          this.ctx.lineTo(xScale(x2), yScale(y2))
          this.ctx.strokeStyle = hexToRgba(s.color, 0.4)
          this.ctx.lineWidth = 1.5
          this.ctx.setLineDash([6, 4])
          this.ctx.stroke()
          this.ctx.setLineDash([])
        }
      }
    }

    // Draw data points
    for (const s of visible) {
      for (const d of s.data) {
        const xVal = typeof d.x === 'number' ? d.x : parseFloat(d.x)
        if (isNaN(xVal)) continue

        const px = xScale(xVal)
        const py = yScale(d.y)

        const isHighlighted =
          this.highlightedPoint !== null &&
          this.highlightedPoint.px === px &&
          this.highlightedPoint.py === py

        this.plottedPoints.push({
          px,
          py,
          dataPoint: d,
          seriesName: s.name,
          color: s.color,
        })

        // Draw dot
        this.ctx.beginPath()
        const radius = isHighlighted ? dotRadius + HIGHLIGHT_RADIUS_BONUS : dotRadius
        this.ctx.arc(px, py, radius, 0, Math.PI * 2)
        this.ctx.fillStyle = isHighlighted ? hexToRgba(s.color, 1) : hexToRgba(s.color, 0.7)
        this.ctx.fill()
        this.ctx.strokeStyle = s.color
        this.ctx.lineWidth = 1.5
        this.ctx.stroke()
      }
    }

    this.updateSeriesLegend()
  }

  private generateNumericLabels(min: number, max: number, step: number): string[] {
    const labels: string[] = []
    for (let v = min; v <= max + step * 0.001; v += step) {
      labels.push(String(Math.round(v * 1000) / 1000))
    }
    return labels
  }

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

    // Find nearest point
    let closest: PlottedPoint | null = null
    let closestDist = Infinity

    for (const pt of this.plottedPoints) {
      const dist = Math.sqrt((mouseX - pt.px) ** 2 + (mouseY - pt.py) ** 2)
      if (dist < closestDist) {
        closestDist = dist
        closest = pt
      }
    }

    const previousHighlighted = this.highlightedPoint

    if (closest !== null && closestDist < HOVER_THRESHOLD) {
      this.highlightedPoint = closest

      if (this.tooltip !== null) {
        let content: string
        if (this.config.tooltip?.format !== undefined) {
          content = this.config.tooltip.format(
            closest.dataPoint.y,
            String(closest.dataPoint.x),
            closest.seriesName
          )
        } else {
          content = `${closest.seriesName}: (${String(closest.dataPoint.x)}, ${String(closest.dataPoint.y)})`
        }
        this.tooltip.show(closest.px, closest.py, content)
      }
    } else {
      this.highlightedPoint = null
      if (this.tooltip !== null) {
        this.tooltip.hide()
      }
    }

    // Re-render if highlight state changed
    if (this.highlightedPoint !== previousHighlighted) {
      this.render()
    }
  }

  protected handleMouseLeave(): void {
    if (this.highlightedPoint !== null) {
      this.highlightedPoint = null
      this.render()
    }
    if (this.tooltip !== null) {
      this.tooltip.hide()
    }
  }

  public update(series: ChartSeries[]): void {
    this.series = this.resolveSeries(series)
    this.render()
  }
}

export type { ScatterChartConfig } from './types'
