/**
 * NetworkGraph - SVG-based network/graph visualization with force, circular, and grid layouts
 * @countermeasure/web-components/visualization/network-graph
 */

import type { GraphEdge, GraphNode, NetworkGraphConfig } from './types'
import { resolveContainer, uniqueId } from '../../primitives/utils'

export type { GraphEdge, GraphNode, NetworkGraphConfig } from './types'

const SVG_NS = 'http://www.w3.org/2000/svg'

const DEFAULT_RADIUS = 20
const FORCE_ITERATIONS = 300
const REPULSION = 5000
const ATTRACTION = 0.005
const CENTER_GRAVITY = 0.01
const DAMPING = 0.9
const ARROWHEAD_SIZE = 8

const DEFAULT_COLORS = [
  '#3b82f6',
  '#ef4444',
  '#10b981',
  '#f59e0b',
  '#8b5cf6',
  '#ec4899',
  '#06b6d4',
  '#f97316',
  '#14b8a6',
  '#6366f1',
]

/** Internal node with mutable position and velocity */
interface SimNode {
  id: string
  label: string
  x: number
  y: number
  vx: number
  vy: number
  radius: number
  color: string
  icon: string | undefined
  group: string | undefined
  data: Record<string, string> | undefined
  original: GraphNode
}

/** Internal edge referencing sim nodes */
interface SimEdge {
  source: SimNode
  target: SimNode
  label: string | undefined
  weight: number
  color: string | undefined
  directed: boolean
  original: GraphEdge
}

export class NetworkGraph {
  public readonly el: HTMLDivElement

  private readonly graphId: string
  private readonly defaultRadius: number
  private readonly showLabels: boolean
  private readonly onNodeClick: ((node: GraphNode) => void) | undefined
  private readonly onEdgeClick: ((edge: GraphEdge) => void) | undefined
  private svg: SVGSVGElement
  private mainGroup: SVGGElement
  private defsEl: SVGDefsElement
  private nodes: GraphNode[]
  private edges: GraphEdge[]
  private simNodes: SimNode[] = []
  private simEdges: SimEdge[] = []
  private layout: 'force' | 'circular' | 'grid'
  private zoomable: boolean
  private pannable: boolean
  private hoveredNodeId: string | null = null

  // View state
  private viewBox: { x: number; y: number; w: number; h: number }
  private isPanning = false
  private panStart = { x: 0, y: 0 }
  private panOrigin = { x: 0, y: 0 }

  // Drag state
  private dragNode: SimNode | null = null
  private dragOffset = { x: 0, y: 0 }

  // Bound handlers
  private readonly boundWheel: (e: WheelEvent) => void
  private readonly boundMouseDown: (e: MouseEvent) => void
  private readonly boundMouseMove: (e: MouseEvent) => void
  private readonly boundMouseUp: () => void

  constructor(config: NetworkGraphConfig) {
    this.graphId = uniqueId('cmm-network-graph')
    this.layout = config.layout ?? 'force'
    this.zoomable = config.zoomable !== false
    this.pannable = config.pannable !== false
    this.defaultRadius = config.nodeRadius ?? DEFAULT_RADIUS
    this.showLabels = config.showLabels !== false
    this.onNodeClick = config.onNodeClick
    this.onEdgeClick = config.onEdgeClick
    this.nodes = [...config.nodes]
    this.edges = [...config.edges]
    this.viewBox = { x: 0, y: 0, w: 800, h: 600 }

    // Build DOM
    this.el = document.createElement('div')
    this.el.id = this.graphId
    this.el.className = this.buildClassName(config.className)

    this.svg = document.createElementNS(SVG_NS, 'svg')
    this.svg.classList.add('cmm-network-graph__svg')

    this.defsEl = document.createElementNS(SVG_NS, 'defs')
    this.svg.appendChild(this.defsEl)

    this.mainGroup = document.createElementNS(SVG_NS, 'g')
    this.svg.appendChild(this.mainGroup)
    this.el.appendChild(this.svg)

    this.createArrowMarker()

    // Mount
    const parent = resolveContainer(config.container)
    parent.appendChild(this.el)

    const rect = this.el.getBoundingClientRect()
    if (rect.width > 0 && rect.height > 0) {
      this.viewBox.w = rect.width
      this.viewBox.h = rect.height
    }

    // Bind handlers
    this.boundWheel = (e: WheelEvent): void => {
      this.handleWheel(e)
    }
    this.boundMouseDown = (e: MouseEvent): void => {
      this.handleMouseDown(e)
    }
    this.boundMouseMove = (e: MouseEvent): void => {
      this.handleMouseMove(e)
    }
    this.boundMouseUp = (): void => {
      this.handleMouseUp()
    }

    if (this.zoomable) {
      this.el.addEventListener('wheel', this.boundWheel, { passive: false })
    }
    this.el.addEventListener('mousedown', this.boundMouseDown)
    document.addEventListener('mousemove', this.boundMouseMove)
    document.addEventListener('mouseup', this.boundMouseUp)

    this.buildSimulation()
    this.applyLayout()
    this.renderGraph()
    this.zoomToFit()
  }

  /** Replace all data and re-render */
  update(nodes: GraphNode[], edges: GraphEdge[]): void {
    this.nodes = [...nodes]
    this.edges = [...edges]
    this.hoveredNodeId = null
    this.dragNode = null
    this.buildSimulation()
    this.applyLayout()
    this.renderGraph()
    this.zoomToFit()
  }

  /** Switch layout algorithm and re-render */
  setLayout(layout: 'force' | 'circular' | 'grid'): void {
    this.layout = layout
    this.applyLayout()
    this.renderGraph()
    this.zoomToFit()
  }

  /** Fit all content in viewport */
  zoomToFit(): void {
    if (this.simNodes.length === 0) return

    let minX = Infinity
    let minY = Infinity
    let maxX = -Infinity
    let maxY = -Infinity

    for (const n of this.simNodes) {
      const r = n.radius
      if (n.x - r < minX) minX = n.x - r
      if (n.y - r < minY) minY = n.y - r
      if (n.x + r > maxX) maxX = n.x + r
      if (n.y + r > maxY) maxY = n.y + r
    }

    const padding = 60
    this.viewBox = {
      x: minX - padding,
      y: minY - padding,
      w: maxX - minX + padding * 2,
      h: maxY - minY + padding * 2,
    }
    this.applyViewBox()
  }

  /** Clean up and remove */
  destroy(): void {
    if (this.zoomable) {
      this.el.removeEventListener('wheel', this.boundWheel)
    }
    this.el.removeEventListener('mousedown', this.boundMouseDown)
    document.removeEventListener('mousemove', this.boundMouseMove)
    document.removeEventListener('mouseup', this.boundMouseUp)
    this.el.remove()
  }

  // ---------------------------------------------------------------------------
  // Simulation setup
  // ---------------------------------------------------------------------------

  private buildSimulation(): void {
    // Map group names to colors
    const groupColors = new Map<string, string>()
    let colorIdx = 0

    this.simNodes = this.nodes.map(n => {
      let color = n.color
      if (color === undefined && n.group !== undefined) {
        const existing = groupColors.get(n.group)
        if (existing !== undefined) {
          color = existing
        } else {
          color = DEFAULT_COLORS[colorIdx % DEFAULT_COLORS.length] ?? '#3b82f6'
          groupColors.set(n.group, color)
          colorIdx++
        }
      }
      if (color === undefined) {
        color = DEFAULT_COLORS[colorIdx % DEFAULT_COLORS.length] ?? '#3b82f6'
        colorIdx++
      }

      return {
        id: n.id,
        label: n.label,
        x: n.x ?? 0,
        y: n.y ?? 0,
        vx: 0,
        vy: 0,
        radius: n.radius ?? this.defaultRadius,
        color,
        icon: n.icon,
        group: n.group,
        data: n.data,
        original: n,
      }
    })

    const nodeMap = new Map<string, SimNode>()
    for (const sn of this.simNodes) {
      nodeMap.set(sn.id, sn)
    }

    this.simEdges = []
    for (const e of this.edges) {
      const source = nodeMap.get(e.source)
      const target = nodeMap.get(e.target)
      if (source === undefined || target === undefined) continue
      this.simEdges.push({
        source,
        target,
        label: e.label,
        weight: e.weight ?? 1,
        color: e.color,
        directed: e.directed === true,
        original: e,
      })
    }
  }

  // ---------------------------------------------------------------------------
  // Layout algorithms
  // ---------------------------------------------------------------------------

  private applyLayout(): void {
    switch (this.layout) {
      case 'force':
        this.forceLayout()
        break
      case 'circular':
        this.circularLayout()
        break
      case 'grid':
        this.gridLayout()
        break
    }
  }

  private forceLayout(): void {
    const cx = this.viewBox.w / 2
    const cy = this.viewBox.h / 2

    // Initialize random positions if not set
    for (const node of this.simNodes) {
      if (node.original.x === undefined || node.original.y === undefined) {
        node.x = cx + (Math.random() - 0.5) * 200
        node.y = cy + (Math.random() - 0.5) * 200
      }
      node.vx = 0
      node.vy = 0
    }

    // Iterative simulation
    for (let iter = 0; iter < FORCE_ITERATIONS; iter++) {
      // Repulsion between all node pairs
      for (let i = 0; i < this.simNodes.length; i++) {
        const a = this.simNodes[i]
        if (a === undefined) continue
        for (let j = i + 1; j < this.simNodes.length; j++) {
          const b = this.simNodes[j]
          if (b === undefined) continue

          let dx = b.x - a.x
          let dy = b.y - a.y
          let dist = Math.sqrt(dx * dx + dy * dy)
          if (dist < 1) {
            dist = 1
            dx = (Math.random() - 0.5) * 2
            dy = (Math.random() - 0.5) * 2
          }

          const force = REPULSION / (dist * dist)
          const fx = (dx / dist) * force
          const fy = (dy / dist) * force

          a.vx -= fx
          a.vy -= fy
          b.vx += fx
          b.vy += fy
        }
      }

      // Attraction along edges
      for (const edge of this.simEdges) {
        const dx = edge.target.x - edge.source.x
        const dy = edge.target.y - edge.source.y
        const dist = Math.sqrt(dx * dx + dy * dy)

        const force = dist * ATTRACTION * edge.weight
        const fx = (dx / Math.max(dist, 1)) * force
        const fy = (dy / Math.max(dist, 1)) * force

        edge.source.vx += fx
        edge.source.vy += fy
        edge.target.vx -= fx
        edge.target.vy -= fy
      }

      // Center gravity
      for (const node of this.simNodes) {
        node.vx += (cx - node.x) * CENTER_GRAVITY
        node.vy += (cy - node.y) * CENTER_GRAVITY
      }

      // Apply velocities with damping
      for (const node of this.simNodes) {
        node.vx *= DAMPING
        node.vy *= DAMPING
        node.x += node.vx
        node.y += node.vy
      }
    }
  }

  private circularLayout(): void {
    const cx = this.viewBox.w / 2
    const cy = this.viewBox.h / 2
    const count = this.simNodes.length
    if (count === 0) return

    const maxRadius = Math.max(...this.simNodes.map(n => n.radius))
    const circumference = count * (maxRadius * 2 + 20)
    const layoutRadius = Math.max(circumference / (2 * Math.PI), 100)

    for (let i = 0; i < count; i++) {
      const node = this.simNodes[i]
      if (node === undefined) continue
      const angle = (2 * Math.PI * i) / count - Math.PI / 2
      node.x = cx + layoutRadius * Math.cos(angle)
      node.y = cy + layoutRadius * Math.sin(angle)
    }
  }

  private gridLayout(): void {
    const count = this.simNodes.length
    if (count === 0) return

    const cols = Math.ceil(Math.sqrt(count))
    const maxRadius = Math.max(...this.simNodes.map(n => n.radius))
    const cellSize = maxRadius * 3 + 20

    for (let i = 0; i < count; i++) {
      const node = this.simNodes[i]
      if (node === undefined) continue
      const col = i % cols
      const row = Math.floor(i / cols)
      node.x = col * cellSize + cellSize / 2
      node.y = row * cellSize + cellSize / 2
    }
  }

  // ---------------------------------------------------------------------------
  // Rendering
  // ---------------------------------------------------------------------------

  private renderGraph(): void {
    while (this.mainGroup.firstChild !== null) {
      this.mainGroup.removeChild(this.mainGroup.firstChild)
    }

    // Edges first
    for (const edge of this.simEdges) {
      this.renderEdge(edge)
    }

    // Nodes on top
    for (const node of this.simNodes) {
      this.renderNode(node)
    }

    this.applyViewBox()
  }

  private renderNode(node: SimNode): void {
    const g = document.createElementNS(SVG_NS, 'g')
    g.classList.add('cmm-network-graph__node')
    g.setAttribute('data-node-id', node.id)

    // Dimming on hover
    if (this.hoveredNodeId !== null && this.hoveredNodeId !== node.id) {
      const isConnected = this.simEdges.some(
        e =>
          (e.source.id === this.hoveredNodeId && e.target.id === node.id) ||
          (e.target.id === this.hoveredNodeId && e.source.id === node.id)
      )
      if (!isConnected) {
        g.classList.add('cmm-network-graph__node--dimmed')
      } else {
        g.classList.add('cmm-network-graph__node--highlighted')
      }
    }

    // Circle
    const circle = document.createElementNS(SVG_NS, 'circle')
    circle.classList.add('cmm-network-graph__node-circle')
    circle.setAttribute('cx', String(node.x))
    circle.setAttribute('cy', String(node.y))
    circle.setAttribute('r', String(node.radius))
    circle.setAttribute('fill', node.color)
    g.appendChild(circle)

    // Icon
    if (node.icon !== undefined) {
      const iconText = document.createElementNS(SVG_NS, 'text')
      iconText.classList.add('cmm-network-graph__node-icon')
      iconText.setAttribute('x', String(node.x))
      iconText.setAttribute('y', String(node.y))
      iconText.textContent = node.icon
      g.appendChild(iconText)
    }

    // Label below
    if (this.showLabels) {
      const label = document.createElementNS(SVG_NS, 'text')
      label.classList.add('cmm-network-graph__label')
      label.setAttribute('x', String(node.x))
      label.setAttribute('y', String(node.y + node.radius + 14))
      label.textContent = node.label
      g.appendChild(label)
    }

    // Hover handlers
    g.addEventListener('mouseenter', () => {
      this.hoveredNodeId = node.id
      this.renderGraph()
    })
    g.addEventListener('mouseleave', () => {
      this.hoveredNodeId = null
      this.renderGraph()
    })

    // Click
    g.addEventListener('click', (e: MouseEvent) => {
      e.stopPropagation()
      this.onNodeClick?.(node.original)
    })

    this.mainGroup.appendChild(g)
  }

  private renderEdge(edge: SimEdge): void {
    const g = document.createElementNS(SVG_NS, 'g')
    g.classList.add('cmm-network-graph__edge')

    // Dimming
    if (this.hoveredNodeId !== null) {
      const isConnected =
        edge.source.id === this.hoveredNodeId || edge.target.id === this.hoveredNodeId
      if (isConnected) {
        g.classList.add('cmm-network-graph__edge--highlighted')
      } else {
        g.classList.add('cmm-network-graph__edge--dimmed')
      }
    }

    const line = document.createElementNS(SVG_NS, 'line')
    line.classList.add('cmm-network-graph__edge-line')
    line.setAttribute('x1', String(edge.source.x))
    line.setAttribute('y1', String(edge.source.y))
    line.setAttribute('x2', String(edge.target.x))
    line.setAttribute('y2', String(edge.target.y))

    if (edge.color !== undefined) {
      line.setAttribute('stroke', edge.color)
    }

    // Weight -> stroke width
    if (edge.weight > 1) {
      line.setAttribute('stroke-width', String(Math.min(edge.weight * 1.5, 6)))
    }

    if (edge.directed) {
      line.setAttribute('marker-end', `url(#${this.graphId}-arrow)`)
    }

    g.appendChild(line)

    // Edge label
    if (edge.label !== undefined) {
      const mx = (edge.source.x + edge.target.x) / 2
      const my = (edge.source.y + edge.target.y) / 2
      const text = document.createElementNS(SVG_NS, 'text')
      text.classList.add('cmm-network-graph__edge-label')
      text.setAttribute('x', String(mx))
      text.setAttribute('y', String(my - 6))
      text.textContent = edge.label
      g.appendChild(text)
    }

    // Click
    g.addEventListener('click', (e: MouseEvent) => {
      e.stopPropagation()
      this.onEdgeClick?.(edge.original)
    })

    this.mainGroup.appendChild(g)
  }

  private createArrowMarker(): void {
    const marker = document.createElementNS(SVG_NS, 'marker')
    marker.setAttribute('id', `${this.graphId}-arrow`)
    marker.setAttribute('markerWidth', String(ARROWHEAD_SIZE))
    marker.setAttribute('markerHeight', String(ARROWHEAD_SIZE))
    marker.setAttribute('refX', String(ARROWHEAD_SIZE))
    marker.setAttribute('refY', String(ARROWHEAD_SIZE / 2))
    marker.setAttribute('orient', 'auto-start-reverse')
    marker.setAttribute('markerUnits', 'userSpaceOnUse')

    const polygon = document.createElementNS(SVG_NS, 'polygon')
    polygon.classList.add('cmm-network-graph__arrowhead')
    polygon.setAttribute(
      'points',
      `0 0, ${String(ARROWHEAD_SIZE)} ${String(ARROWHEAD_SIZE / 2)}, 0 ${String(ARROWHEAD_SIZE)}`
    )
    marker.appendChild(polygon)
    this.defsEl.appendChild(marker)
  }

  // ---------------------------------------------------------------------------
  // Zoom / Pan / Drag
  // ---------------------------------------------------------------------------

  private applyViewBox(): void {
    this.svg.setAttribute(
      'viewBox',
      `${String(this.viewBox.x)} ${String(this.viewBox.y)} ${String(this.viewBox.w)} ${String(this.viewBox.h)}`
    )
  }

  private handleWheel(e: WheelEvent): void {
    e.preventDefault()
    const factor = e.deltaY > 0 ? 1.1 : 0.9

    const rect = this.el.getBoundingClientRect()
    const mx = ((e.clientX - rect.left) / rect.width) * this.viewBox.w + this.viewBox.x
    const my = ((e.clientY - rect.top) / rect.height) * this.viewBox.h + this.viewBox.y

    const newW = this.viewBox.w * factor
    const newH = this.viewBox.h * factor

    this.viewBox.x = mx - (mx - this.viewBox.x) * (newW / this.viewBox.w)
    this.viewBox.y = my - (my - this.viewBox.y) * (newH / this.viewBox.h)
    this.viewBox.w = newW
    this.viewBox.h = newH

    this.applyViewBox()
  }

  private handleMouseDown(e: MouseEvent): void {
    if (e.button !== 0) return

    // Check if clicking on a node for dragging
    const target = e.target as Element | null
    const nodeGroup = target !== null ? target.closest('.cmm-network-graph__node') : null

    if (nodeGroup !== null) {
      const nodeId = nodeGroup.getAttribute('data-node-id')
      if (nodeId !== null) {
        const simNode = this.simNodes.find(n => n.id === nodeId)
        if (simNode !== undefined) {
          this.dragNode = simNode
          const svgPt = this.clientToSvg(e.clientX, e.clientY)
          this.dragOffset = {
            x: simNode.x - svgPt.x,
            y: simNode.y - svgPt.y,
          }
          return
        }
      }
    }

    // Otherwise: pan
    if (this.pannable) {
      this.isPanning = true
      this.panStart = { x: e.clientX, y: e.clientY }
      this.panOrigin = { x: this.viewBox.x, y: this.viewBox.y }
    }
  }

  private handleMouseMove(e: MouseEvent): void {
    if (this.dragNode !== null) {
      const svgPt = this.clientToSvg(e.clientX, e.clientY)
      this.dragNode.x = svgPt.x + this.dragOffset.x
      this.dragNode.y = svgPt.y + this.dragOffset.y
      this.renderGraph()
      return
    }

    if (this.isPanning) {
      const rect = this.el.getBoundingClientRect()
      const scaleX = this.viewBox.w / rect.width
      const scaleY = this.viewBox.h / rect.height

      this.viewBox.x = this.panOrigin.x - (e.clientX - this.panStart.x) * scaleX
      this.viewBox.y = this.panOrigin.y - (e.clientY - this.panStart.y) * scaleY

      this.applyViewBox()
    }
  }

  private handleMouseUp(): void {
    this.dragNode = null
    this.isPanning = false
  }

  private clientToSvg(clientX: number, clientY: number): { x: number; y: number } {
    const rect = this.el.getBoundingClientRect()
    return {
      x: ((clientX - rect.left) / rect.width) * this.viewBox.w + this.viewBox.x,
      y: ((clientY - rect.top) / rect.height) * this.viewBox.h + this.viewBox.y,
    }
  }

  // ---------------------------------------------------------------------------
  // Helpers
  // ---------------------------------------------------------------------------

  private buildClassName(extra: string | undefined): string {
    const classes = ['cmm-network-graph']
    if (extra !== undefined) {
      classes.push(extra)
    }
    return classes.join(' ')
  }
}
