/**
 * Funnel chart using Canvas 2D
 * @countermeasure/web-components/charts/funnel
 */

import { BaseChart } from '../base'
import { defaultColors, hexToRgba, formatNumber } from '../utils'
import { ChartTooltip } from '../tooltip'
import type { FunnelChartConfig, FunnelSegment } from './types'

const LABEL_COLOR = '#334155'
const VALUE_COLOR = '#64748b'
const LABEL_FONT = '12px system-ui, sans-serif'
const VALUE_FONT = '11px system-ui, sans-serif'
const HOVER_ALPHA_DELTA = 0.12

interface ResolvedSegment {
  label: string
  value: number
  color: string
  /** Width ratio relative to max value (0-1) */
  widthRatio: number
  /** Percentage of first segment value */
  percentage: number
}

export class FunnelChart extends BaseChart {
  private segments: FunnelSegment[]
  private resolved: ResolvedSegment[] = []
  private colors: string[]
  private showLabels: boolean
  private showValues: boolean
  private direction: 'vertical' | 'horizontal'
  private curved: boolean
  private gap: number

  private tooltip: ChartTooltip | null = null
  private hoveredIndex = -1
  private boundMouseMove: (e: MouseEvent) => void
  private boundMouseLeave: () => void

  constructor(config: FunnelChartConfig) {
    super(config)
    this.segments = config.segments
    this.colors = defaultColors()
    this.showLabels = config.showLabels ?? true
    this.showValues = config.showValues ?? true
    this.direction = config.direction ?? 'vertical'
    this.curved = config.curved ?? false
    this.gap = config.gap ?? 2

    if (config.tooltip?.enabled !== false) {
      this.tooltip = new ChartTooltip(this.container)
    }

    this.boundMouseMove = (e: MouseEvent) => {
      this.handleMouseMove(e)
    }
    this.boundMouseLeave = () => {
      this.handleMouseLeave()
    }

    this.canvas.addEventListener('mousemove', this.boundMouseMove)
    this.canvas.addEventListener('mouseleave', this.boundMouseLeave)

    this.resolveSegments()
    this.render()
  }

  /** Replace the segments and re-render */
  update(segments: FunnelSegment[]): void {
    this.segments = segments
    this.resolveSegments()
    this.render()
  }

  render(): void {
    this.clear()

    if (this.resolved.length === 0) return

    if (this.direction === 'horizontal') {
      this.renderHorizontal()
    } else {
      this.renderVertical()
    }
  }

  override destroy(): void {
    this.canvas.removeEventListener('mousemove', this.boundMouseMove)
    this.canvas.removeEventListener('mouseleave', this.boundMouseLeave)
    if (this.tooltip !== null) {
      this.tooltip.destroy()
    }
    super.destroy()
  }

  private resolveSegments(): void {
    const maxVal = this.findMaxValue()
    const firstVal = this.segments[0]?.value ?? 1

    this.resolved = []

    for (let i = 0; i < this.segments.length; i++) {
      const seg = this.segments[i]
      if (seg === undefined) continue

      const color = seg.color ?? this.colors[i % this.colors.length] ?? '#3b82f6'
      const widthRatio = maxVal > 0 ? seg.value / maxVal : 0
      const percentage = firstVal > 0 ? (seg.value / firstVal) * 100 : 0

      this.resolved.push({
        label: seg.label,
        value: seg.value,
        color,
        widthRatio,
        percentage,
      })
    }
  }

  private findMaxValue(): number {
    let max = 0
    for (const seg of this.segments) {
      if (seg.value > max) {
        max = seg.value
      }
    }
    return max
  }

  private renderVertical(): void {
    const plot = this.plotArea
    const count = this.resolved.length
    if (count === 0) return

    const totalGap = this.gap * Math.max(0, count - 1)
    const segmentHeight = (plot.height - totalGap) / count

    for (let i = 0; i < count; i++) {
      const seg = this.resolved[i]
      if (seg === undefined) continue

      const nextSeg = this.resolved[i + 1]
      const currentWidth = seg.widthRatio * plot.width
      const nextWidth = nextSeg !== undefined ? nextSeg.widthRatio * plot.width : currentWidth * 0.7

      const y = plot.y + i * (segmentHeight + this.gap)
      const cx = plot.x + plot.width / 2

      const topLeft = cx - currentWidth / 2
      const topRight = cx + currentWidth / 2
      const bottomLeft = cx - nextWidth / 2
      const bottomRight = cx + nextWidth / 2

      const isHovered = i === this.hoveredIndex
      const fillColor = isHovered ? hexToRgba(seg.color, 1 - HOVER_ALPHA_DELTA) : seg.color

      this.ctx.save()
      this.ctx.beginPath()

      if (this.curved) {
        const cpY = y + segmentHeight * 0.6
        this.ctx.moveTo(topLeft, y)
        this.ctx.lineTo(topRight, y)
        this.ctx.quadraticCurveTo(topRight, cpY, bottomRight, y + segmentHeight)
        this.ctx.lineTo(bottomLeft, y + segmentHeight)
        this.ctx.quadraticCurveTo(topLeft, cpY, topLeft, y)
      } else {
        this.ctx.moveTo(topLeft, y)
        this.ctx.lineTo(topRight, y)
        this.ctx.lineTo(bottomRight, y + segmentHeight)
        this.ctx.lineTo(bottomLeft, y + segmentHeight)
      }

      this.ctx.closePath()
      this.ctx.fillStyle = fillColor
      this.ctx.fill()
      this.ctx.restore()

      // Draw text
      this.drawSegmentText(cx, y + segmentHeight / 2, seg)
    }
  }

  private renderHorizontal(): void {
    const plot = this.plotArea
    const count = this.resolved.length
    if (count === 0) return

    const totalGap = this.gap * Math.max(0, count - 1)
    const segmentWidth = (plot.width - totalGap) / count

    for (let i = 0; i < count; i++) {
      const seg = this.resolved[i]
      if (seg === undefined) continue

      const nextSeg = this.resolved[i + 1]
      const currentHeight = seg.widthRatio * plot.height
      const nextHeight =
        nextSeg !== undefined ? nextSeg.widthRatio * plot.height : currentHeight * 0.7

      const x = plot.x + i * (segmentWidth + this.gap)
      const cy = plot.y + plot.height / 2

      const leftTop = cy - currentHeight / 2
      const leftBottom = cy + currentHeight / 2
      const rightTop = cy - nextHeight / 2
      const rightBottom = cy + nextHeight / 2

      const isHovered = i === this.hoveredIndex
      const fillColor = isHovered ? hexToRgba(seg.color, 1 - HOVER_ALPHA_DELTA) : seg.color

      this.ctx.save()
      this.ctx.beginPath()

      if (this.curved) {
        const cpX = x + segmentWidth * 0.6
        this.ctx.moveTo(x, leftTop)
        this.ctx.lineTo(x, leftBottom)
        this.ctx.quadraticCurveTo(cpX, leftBottom, x + segmentWidth, rightBottom)
        this.ctx.lineTo(x + segmentWidth, rightTop)
        this.ctx.quadraticCurveTo(cpX, leftTop, x, leftTop)
      } else {
        this.ctx.moveTo(x, leftTop)
        this.ctx.lineTo(x, leftBottom)
        this.ctx.lineTo(x + segmentWidth, rightBottom)
        this.ctx.lineTo(x + segmentWidth, rightTop)
      }

      this.ctx.closePath()
      this.ctx.fillStyle = fillColor
      this.ctx.fill()
      this.ctx.restore()

      // Draw text
      this.drawSegmentText(x + segmentWidth / 2, cy, seg)
    }
  }

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

    if (this.showLabels && this.showValues) {
      this.ctx.fillStyle = LABEL_COLOR
      this.ctx.font = LABEL_FONT
      this.ctx.fillText(seg.label, cx, cy - 8)

      this.ctx.fillStyle = VALUE_COLOR
      this.ctx.font = VALUE_FONT
      this.ctx.fillText(`${formatNumber(seg.value)} (${seg.percentage.toFixed(1)}%)`, cx, cy + 8)
    } else if (this.showLabels) {
      this.ctx.fillStyle = LABEL_COLOR
      this.ctx.font = LABEL_FONT
      this.ctx.fillText(seg.label, cx, cy)
    } else if (this.showValues) {
      this.ctx.fillStyle = VALUE_COLOR
      this.ctx.font = VALUE_FONT
      this.ctx.fillText(formatNumber(seg.value), cx, cy)
    }

    this.ctx.restore()
  }

  private 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 count = this.resolved.length
    if (count === 0) return

    let found = -1

    if (this.direction === 'horizontal') {
      const totalGap = this.gap * Math.max(0, count - 1)
      const segmentWidth = (plot.width - totalGap) / count
      const relX = mx - plot.x

      if (relX >= 0 && relX <= plot.width && my >= plot.y && my <= plot.y + plot.height) {
        for (let i = 0; i < count; i++) {
          const segStart = i * (segmentWidth + this.gap)
          const segEnd = segStart + segmentWidth
          if (relX >= segStart && relX < segEnd) {
            found = i
            break
          }
        }
      }
    } else {
      const totalGap = this.gap * Math.max(0, count - 1)
      const segmentHeight = (plot.height - totalGap) / count
      const relY = my - plot.y

      if (relY >= 0 && relY <= plot.height && mx >= plot.x && mx <= plot.x + plot.width) {
        for (let i = 0; i < count; i++) {
          const segStart = i * (segmentHeight + this.gap)
          const segEnd = segStart + segmentHeight
          if (relY >= segStart && relY < segEnd) {
            found = i
            break
          }
        }
      }
    }

    if (found !== this.hoveredIndex) {
      this.hoveredIndex = found
      this.render()
    }

    if (found !== -1 && this.tooltip !== null) {
      const seg = this.resolved[found]
      if (seg !== undefined) {
        const content = `<strong>${seg.label}</strong><br/>${formatNumber(seg.value)} (${seg.percentage.toFixed(1)}%)`
        this.tooltip.show(mx, my, content)
      }
    } else if (this.tooltip !== null) {
      this.tooltip.hide()
    }
  }

  private handleMouseLeave(): void {
    if (this.hoveredIndex !== -1) {
      this.hoveredIndex = -1
      if (this.tooltip !== null) {
        this.tooltip.hide()
      }
      this.render()
    }
  }
}
