'use client'

import { useMemo, useRef, useState, type PointerEvent as ReactPointerEvent, type WheelEvent } from 'react'

import { useI18n } from '@/components/providers/i18n-provider'
import { Badge } from '@/components/ui/display'
import { cn } from '@/lib/cn'
import {
  BACKBONE_LINKS,
  MAP_HEIGHT,
  MAP_WIDTH,
  SAUDI_OUTLINE_PATH,
  project,
} from '@/lib/data/saudi-geometry'
import { GRID_STATES, type GridState } from '@/lib/domain/enums'
import { STATE_STYLES } from '@/lib/ui/state-styles'

export interface MapAsset {
  code: string
  name: string
  nameAr: string
  typeKey: string
  regionCode: string
  lat: number
  lon: number
  risk: number
  state: GridState
  loadPct: number
  tempC: number
  capacityMw: number
  /** Risk an hour out, for the failure-prediction layer (§10). */
  predictedRisk?: number
  /** Days past the maintenance interval, for the maintenance layer. */
  maintenanceDebtDays?: number
}

export interface MapRegion {
  code: string
  name: string
  nameAr: string
  lat: number
  lon: number
  peakRisk: number
  state: GridState
  loadMw: number
}

const MIN_ZOOM = 0.85
const MAX_ZOOM = 9

/**
 * The national GIS view.
 *
 * Rendered as a single SVG with a pan/zoom transform rather than a tiled map: it needs
 * no network, redraws instantly when live risk changes, and lets asset markers, the
 * risk heat layer and the transmission backbone share one coordinate space.
 */
export function NationalMap({
  assets,
  regions,
  selectedCode,
  onSelect,
  showHeatmap = true,
  showLines = true,
  showLabels = true,
  layer = 'risk',
  className,
  compact = false,
}: {
  assets: MapAsset[]
  regions: MapRegion[]
  selectedCode?: string | null
  onSelect?: (asset: MapAsset | null) => void
  showHeatmap?: boolean
  showLines?: boolean
  showLabels?: boolean
  /**
   * What the markers are coloured by. Every option is a real reading the platform
   * already computes; there is no layer here that only changes the legend.
   */
  layer?: 'risk' | 'load' | 'prediction' | 'maintenance'
  className?: string
  compact?: boolean
}) {
  const { t, locale, n } = useI18n()
  const svgRef = useRef<SVGSVGElement>(null)
  const [view, setView] = useState({ scale: 1, x: 0, y: 0 })
  const dragRef = useRef<{ x: number; y: number; vx: number; vy: number } | null>(null)
  const [hovered, setHovered] = useState<MapAsset | null>(null)

  const regionByCode = useMemo(
    () => new Map(regions.map((region) => [region.code, region])),
    [regions],
  )

  const projected = useMemo(
    () =>
      assets.map((asset) => ({
        asset,
        point: project(asset.lon, asset.lat),
        // Marker radius follows capacity, so a 750 MVA bulk unit reads as more
        // consequential than a 63 MVA distribution transformer at a glance.
        radius: Math.max(2.6, Math.min(7, 2.2 + Math.sqrt(asset.capacityMw) / 9)),
      })),
    [assets],
  )

  const onWheel = (event: WheelEvent<SVGSVGElement>) => {
    event.preventDefault()
    const rect = svgRef.current?.getBoundingClientRect()
    if (!rect) return

    const factor = event.deltaY < 0 ? 1.18 : 1 / 1.18
    const nextScale = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, view.scale * factor))
    if (nextScale === view.scale) return

    // Zoom about the pointer rather than the centre, so the feature under the cursor
    // stays put — the behaviour every map has trained people to expect.
    const px = ((event.clientX - rect.left) / rect.width) * MAP_WIDTH
    const py = ((event.clientY - rect.top) / rect.height) * MAP_HEIGHT
    const ratio = nextScale / view.scale

    setView({
      scale: nextScale,
      x: px - (px - view.x) * ratio,
      y: py - (py - view.y) * ratio,
    })
  }

  const onPointerDown = (event: ReactPointerEvent<SVGSVGElement>) => {
    if (event.button !== 0) return
    ;(event.target as Element).setPointerCapture?.(event.pointerId)
    dragRef.current = { x: event.clientX, y: event.clientY, vx: view.x, vy: view.y }
  }

  const onPointerMove = (event: ReactPointerEvent<SVGSVGElement>) => {
    const drag = dragRef.current
    if (!drag) return
    const rect = svgRef.current?.getBoundingClientRect()
    if (!rect) return
    const dx = ((event.clientX - drag.x) / rect.width) * MAP_WIDTH
    const dy = ((event.clientY - drag.y) / rect.height) * MAP_HEIGHT
    setView((current) => ({ ...current, x: drag.vx + dx, y: drag.vy + dy }))
  }

  const endDrag = () => {
    dragRef.current = null
  }

  const reset = () => setView({ scale: 1, x: 0, y: 0 })
  const zoomBy = (factor: number) =>
    setView((current) => {
      const nextScale = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, current.scale * factor))
      const ratio = nextScale / current.scale
      const cx = MAP_WIDTH / 2
      const cy = MAP_HEIGHT / 2
      return {
        scale: nextScale,
        x: cx - (cx - current.x) * ratio,
        y: cy - (cy - current.y) * ratio,
      }
    })

  // Busiest region on screen, so corridor thickness is relative to what is shown
  // rather than to a constant that would be wrong at regional zoom.
  const maxRegionLoadMw = Math.max(1, ...regions.map((region) => region.loadMw))

  return (
    <div className={cn('relative overflow-hidden rounded-[--radius-panel] bg-[#060a12]', className)}>
      <svg
        ref={svgRef}
        viewBox={`0 0 ${MAP_WIDTH} ${MAP_HEIGHT}`}
        className="h-full w-full touch-none select-none"
        style={{ cursor: dragRef.current ? 'grabbing' : 'grab' }}
        onWheel={onWheel}
        onPointerDown={onPointerDown}
        onPointerMove={onPointerMove}
        onPointerUp={endDrag}
        onPointerLeave={endDrag}
        role="img"
        aria-label={t('a11y.mapDescription')}
      >
        <defs>
          <pattern id="mapGrid" width="40" height="40" patternUnits="userSpaceOnUse">
            <path d="M40 0H0v40" fill="none" stroke="#ffffff" strokeOpacity="0.035" strokeWidth="1" />
          </pattern>
          {GRID_STATES.map((state) => (
            <radialGradient key={state} id={`heat-${state}`}>
              <stop offset="0%" stopColor={STATE_STYLES[state].hex} stopOpacity="0.42" />
              <stop offset="55%" stopColor={STATE_STYLES[state].hex} stopOpacity="0.14" />
              <stop offset="100%" stopColor={STATE_STYLES[state].hex} stopOpacity="0" />
            </radialGradient>
          ))}
          <filter id="markerGlow" x="-60%" y="-60%" width="220%" height="220%">
            <feGaussianBlur stdDeviation="3" result="blur" />
            <feMerge>
              <feMergeNode in="blur" />
              <feMergeNode in="SourceGraphic" />
            </feMerge>
          </filter>
        </defs>

        <rect width={MAP_WIDTH} height={MAP_HEIGHT} fill="url(#mapGrid)" />

        <g transform={`translate(${view.x} ${view.y}) scale(${view.scale})`}>
          {/* Landmass */}
          <path
            d={SAUDI_OUTLINE_PATH}
            fill="#0c1524"
            stroke="#26507a"
            strokeWidth={1.4 / view.scale}
            strokeLinejoin="round"
          />

          {/* Risk heat layer */}
          {showHeatmap
            ? regions.map((region) => {
                const { x, y } = project(region.lon, region.lat)
                const radius = 34 + (region.peakRisk / 100) * 52
                return (
                  <circle
                    key={`heat-${region.code}`}
                    cx={x}
                    cy={y}
                    r={radius}
                    fill={`url(#heat-${region.state})`}
                    style={{ transition: 'r 700ms ease-out' }}
                  />
                )
              })
            : null}

          {/* Transmission backbone with animated energy flow (§9).

              Three readings on one line: thickness is how much is moving, colour is the
              worst state at either end, and the dash animation runs in the direction the
              power is travelling — from the region with the lighter load towards the
              heavier one. Direction is the reading operators check first, and a static
              line cannot carry it at all. */}
          {showLines
            ? BACKBONE_LINKS.map(([from, to]) => {
                const a = regionByCode.get(from)
                const b = regionByCode.get(to)
                if (!a || !b) return null
                const p1 = project(a.lon, a.lat)
                const p2 = project(b.lon, b.lat)

                const worst = Math.max(a.peakRisk, b.peakRisk)
                const critical = worst >= 75
                const stressed = worst >= 55
                const colour = critical
                  ? STATE_STYLES.critical.hex
                  : stressed
                    ? STATE_STYLES.warning.hex
                    : '#2b7d5c'

                // Heavier corridors are drawn thicker, normalised against the busiest
                // region on screen so the scale adapts to what is actually being shown.
                const share = Math.min(1, (a.loadMw + b.loadMw) / Math.max(1, maxRegionLoadMw * 2))
                const width = (1.1 + share * 2.6) / view.scale
                // Reversed when the flow runs the other way, so the dashes always travel
                // towards the heavier end.
                const reversed = a.loadMw > b.loadMw

                return (
                  <g key={`${from}-${to}`}>
                    <line
                      x1={p1.x}
                      y1={p1.y}
                      x2={p2.x}
                      y2={p2.y}
                      stroke={colour}
                      strokeOpacity={stressed ? 0.55 : 0.35}
                      strokeWidth={width}
                      strokeLinecap="round"
                    />
                    <line
                      x1={p1.x}
                      y1={p1.y}
                      x2={p2.x}
                      y2={p2.y}
                      stroke={colour}
                      strokeOpacity={0.9}
                      strokeWidth={width * 0.7}
                      strokeLinecap="round"
                      className={cn(
                        reversed ? 'nabdh-flow-reverse' : 'nabdh-flow',
                        critical ? 'nabdh-flow-fast' : stressed ? undefined : 'nabdh-flow-slow',
                      )}
                    />
                  </g>
                )
              })
            : null}

          {/* Assets */}
          {projected.map(({ asset, point, radius }) => {
            // What the marker is coloured by follows the active layer. Each option reads
            // a different quantity the platform already computes, so switching layers
            // genuinely changes which assets stand out.
            const reading =
              layer === 'load'
                ? Math.abs(asset.loadPct)
                : layer === 'prediction'
                  ? (asset.predictedRisk ?? asset.risk)
                  : layer === 'maintenance'
                    ? Math.min(100, ((asset.maintenanceDebtDays ?? 0) / 240) * 100)
                    : asset.risk
            const style =
              layer === 'risk'
                ? STATE_STYLES[asset.state]
                : STATE_STYLES[
                    reading >= 78 ? 'critical' : reading >= 60 ? 'warning' : reading >= 40 ? 'watch' : 'normal'
                  ]
            const selected = selectedCode === asset.code
            const alert = reading >= 75
            return (
              <g key={asset.code}>
                {alert ? (
                  <circle
                    cx={point.x}
                    cy={point.y}
                    r={radius * 2.4}
                    fill="none"
                    stroke={style.hex}
                    strokeOpacity="0.5"
                    strokeWidth={1.2 / view.scale}
                    className="nabdh-pulse"
                  />
                ) : null}
                <circle
                  cx={point.x}
                  cy={point.y}
                  r={selected ? radius * 1.6 : radius}
                  fill={style.hex}
                  fillOpacity={selected ? 1 : 0.9}
                  stroke={selected ? '#ffffff' : '#05080f'}
                  strokeWidth={(selected ? 2 : 1) / view.scale}
                  filter={alert ? 'url(#markerGlow)' : undefined}
                  style={{ cursor: 'pointer' }}
                  onPointerEnter={() => setHovered(asset)}
                  onPointerLeave={() => setHovered(null)}
                  onClick={(event) => {
                    event.stopPropagation()
                    onSelect?.(selected ? null : asset)
                  }}
                />
              </g>
            )
          })}

          {/* Region labels */}
          {showLabels
            ? regions.map((region) => {
                const { x, y } = project(region.lon, region.lat)
                // Lift the label clear of the heat blob, which grows with risk — a fixed
                // offset left the busiest regions with their name sitting inside the glow,
                // exactly where it is least readable.
                const labelOffset = 20 + (region.peakRisk / 100) * 22
                return (
                  <text
                    key={`label-${region.code}`}
                    x={x}
                    y={y - labelOffset}
                    textAnchor="middle"
                    fontSize={11 / Math.max(1, view.scale * 0.65)}
                    fill="#c9d7ee"
                    stroke="#05080f"
                    strokeWidth={2.5 / view.scale}
                    paintOrder="stroke"
                    className="pointer-events-none font-medium"
                  >
                    {locale === 'ar' ? region.nameAr : region.name}
                  </text>
                )
              })
            : null}
        </g>
      </svg>

      {/* Hover readout */}
      {hovered ? (
        <div className="pointer-events-none absolute bottom-3 start-3 rounded-lg border border-border-strong bg-surface/95 px-3 py-2 backdrop-blur">
          <p className="text-xs font-semibold text-text">
            {hovered.code} · {locale === 'ar' ? hovered.nameAr : hovered.name}
          </p>
          <p className="mt-0.5 text-[11px] text-text-muted">
            {t(`assetType.${hovered.typeKey}`)} · {t('common.risk')}{' '}
            <span className={STATE_STYLES[hovered.state].text}>
              {n(hovered.risk, { maximumFractionDigits: 0 })}%
            </span>{' '}
            · {n(hovered.loadPct, { maximumFractionDigits: 0 })}%
          </p>
        </div>
      ) : null}

      {/* Controls */}
      <div className="absolute end-3 top-3 flex flex-col gap-1">
        <MapButton label={t('map.zoomIn')} onClick={() => zoomBy(1.4)}>
          +
        </MapButton>
        <MapButton label={t('map.zoomOut')} onClick={() => zoomBy(1 / 1.4)}>
          −
        </MapButton>
        <MapButton label={t('map.resetView')} onClick={reset}>
          <svg viewBox="0 0 20 20" className="size-3.5" fill="none" stroke="currentColor" strokeWidth="1.8">
            <path d="M4 10a6 6 0 1 1 2 4.5" strokeLinecap="round" />
            <path d="M4 6v4h4" strokeLinecap="round" strokeLinejoin="round" />
          </svg>
        </MapButton>
      </div>

      {/* Legend */}
      {!compact ? (
        <div className="absolute bottom-3 end-3 rounded-lg border border-border bg-surface/90 px-3 py-2 backdrop-blur">
          <p className="mb-1.5 text-[10px] font-semibold uppercase tracking-wide text-text-faint">
            {t('map.legend')}
          </p>
          <div className="flex flex-wrap gap-x-3 gap-y-1">
            {GRID_STATES.map((state) => (
              <span key={state} className="flex items-center gap-1.5 text-[11px] text-text-muted">
                <span
                  className="size-2 rounded-full"
                  style={{ backgroundColor: STATE_STYLES[state].hex }}
                />
                {t(`states.${state}`)}
              </span>
            ))}
          </div>
        </div>
      ) : null}

      <div className="absolute start-3 top-3">
        <Badge tone="muted">{t('map.assetsShown', { count: assets.length })}</Badge>
      </div>
    </div>
  )
}

function MapButton({
  children,
  label,
  onClick,
}: {
  children: React.ReactNode
  label: string
  onClick: () => void
}) {
  return (
    <button
      type="button"
      onClick={onClick}
      aria-label={label}
      title={label}
      className="flex size-7 items-center justify-center rounded-md border border-border bg-surface/90 text-sm text-text-muted backdrop-blur transition-colors hover:border-border-strong hover:text-text"
    >
      {children}
    </button>
  )
}
