'use client'

import { useMemo } from 'react'

import { STATE_STYLES } from '@/lib/ui/state-styles'
import type { GridState } from '@/lib/domain/enums'
import { cn } from '@/lib/cn'
import type { GridLayer, SceneAsset, SceneEdge, SceneNode } from './types'

/**
 * The 2D network twin (§88, §90).
 *
 * Not a placeholder. This is the view when WebGL is unavailable, when the device is a
 * phone, or when an operator simply prefers a one-line diagram — and it carries the same
 * data the 3D scene does: the same nodes, the same solved flow, the same layer colouring,
 * the same click target, the same labels. What it drops is depth and animation, not
 * information.
 *
 * SVG rather than canvas because every asset is then a real element: focusable, reachable
 * by keyboard, and readable by a screen reader without a parallel accessibility tree.
 */

const RAMP = ['#2c4a6e', '#38bdf8', '#24d07f', '#fbbf24', '#f97316', '#ef4444']

function ramp(fraction: number): string {
  return RAMP[Math.floor(Math.min(0.999, Math.max(0, fraction)) * RAMP.length)]
}

/** The same mapping the 3D scene uses, so switching views never changes an answer. */
function colourFor(layer: GridLayer, asset: SceneAsset | undefined): string {
  if (!asset) return '#33445c'
  if (asset.isOutage) return '#4b5563'
  switch (layer) {
    case 'voltage': {
      const deviation = Math.abs(asset.voltageDeviationPct)
      return deviation >= 10 ? '#ef4444' : deviation >= 5 ? '#f97316' : deviation >= 2 ? '#fbbf24' : '#24d07f'
    }
    case 'current':
      return ramp(Math.abs(asset.currentA) / Math.max(1, asset.ratedCurrentA))
    case 'load':
      return ramp(Math.abs(asset.loadPct) / 120)
    case 'frequency': {
      const off = Math.abs(asset.frequencyHz - 60)
      return off >= 0.4 ? '#ef4444' : off >= 0.2 ? '#f97316' : off >= 0.08 ? '#fbbf24' : '#24d07f'
    }
    case 'temperature':
      return ramp(asset.thermalStress)
    case 'health':
      return ramp(1 - asset.healthScore / 100)
    case 'risk':
      return ramp(asset.riskScore / 100)
    case 'failure_dna':
      return asset.dnaMatchPct >= 70 ? '#a78bfa' : asset.dnaMatchPct >= 45 ? '#6d5bd0' : '#33445c'
    case 'cascade':
      return asset.cascadeDepth === null ? '#2a3446' : ramp(1 - asset.cascadeDepth / 5)
    case 'predicted':
      return ramp((asset.predictedRisk ?? asset.riskScore) / 100)
    case 'maintenance':
      return asset.hasOpenWorkOrder ? '#fbbf24' : '#2f7d5e'
    case 'data_quality':
      return ramp(1 - asset.dataQualityPct / 100)
    default:
      return STATE_STYLES[(asset.state as GridState) in STATE_STYLES ? (asset.state as GridState) : 'normal'].hex
  }
}

const EDGE_COLOUR: Record<string, string> = {
  normal: '#2f7d5e',
  watch: STATE_STYLES.watch.hex,
  warning: STATE_STYLES.warning.hex,
  critical: STATE_STYLES.critical.hex,
}

/** Radius in the projected plan, by archetype — a plant reads bigger than a sensor. */
const RADIUS: Record<string, number> = {
  plant: 13,
  substation: 12,
  transformer: 9,
  battery: 9,
  solar: 10,
  wind: 8,
  load: 9,
  ev: 7,
  line: 6,
  busbar: 7,
  building: 6,
  box: 7,
}

export function LiveGrid2D({
  nodes,
  edges,
  assets,
  layer,
  selectedCode,
  highlight,
  showLabels,
  reducedMotion,
  onSelect,
}: {
  nodes: SceneNode[]
  edges: SceneEdge[]
  assets: SceneAsset[]
  layer: GridLayer
  selectedCode: string | null
  highlight: string[] | null
  showLabels: boolean
  reducedMotion: boolean
  onSelect: (code: string) => void
}) {
  const assetByCode = useMemo(() => new Map(assets.map((asset) => [asset.code, asset])), [assets])
  const nodeByCode = useMemo(() => new Map(nodes.map((node) => [node.code, node])), [nodes])
  const highlighted = useMemo(() => (highlight ? new Set(highlight) : null), [highlight])

  // Fit the plan to its own extent rather than to a fixed viewBox: a substation twin and
  // a national twin differ by three orders of magnitude in metres.
  const bounds = useMemo(() => {
    if (nodes.length === 0) return { minX: -100, maxX: 100, minZ: -100, maxZ: 100 }
    const xs = nodes.map((node) => node.x)
    const zs = nodes.map((node) => node.z)
    const pad = 40
    return {
      minX: Math.min(...xs) - pad,
      maxX: Math.max(...xs) + pad,
      minZ: Math.min(...zs) - pad,
      maxZ: Math.max(...zs) + pad,
    }
  }, [nodes])

  const width = Math.max(1, bounds.maxX - bounds.minX)
  const height = Math.max(1, bounds.maxZ - bounds.minZ)

  if (nodes.length === 0) {
    return (
      <div className="flex h-full items-center justify-center text-[12px] text-text-muted">
        —
      </div>
    )
  }

  return (
    <svg
      viewBox={`${bounds.minX} ${bounds.minZ} ${width} ${height}`}
      className="h-full w-full"
      role="img"
      aria-label="Network diagram"
      data-testid="live-grid-2d"
    >
      <rect x={bounds.minX} y={bounds.minZ} width={width} height={height} fill="#060a11" />

      {/* Conductors, drawn before the assets so the assets sit on top of them. */}
      <g>
        {edges.map((edge) => {
          const from = nodeByCode.get(edge.sourceCode)
          const to = nodeByCode.get(edge.sinkCode)
          if (!from || !to) return null
          const dim = highlighted && !highlighted.has(edge.from) && !highlighted.has(edge.to)
          return (
            <g key={`${edge.from}-${edge.to}`} opacity={dim ? 0.15 : 1}>
              <line
                x1={from.x}
                y1={from.z}
                x2={to.x}
                y2={to.z}
                stroke={EDGE_COLOUR[edge.state] ?? EDGE_COLOUR.normal}
                strokeWidth={1.5 + edge.intensity * 5}
                strokeLinecap="round"
                opacity={edge.direction === 'idle' ? 0.25 : 0.85}
              />
              {/*
                Direction as a dashed march rather than as an arrowhead: an arrow at the
                midpoint is invisible at national scale, and the dash pattern reads as
                movement at any zoom. The animation is dropped under reduced motion, and
                the dashes stay — direction is still legible from the offset.
              */}
              {edge.direction !== 'idle' ? (
                <line
                  x1={from.x}
                  y1={from.z}
                  x2={to.x}
                  y2={to.z}
                  stroke="#e6f2ff"
                  strokeWidth={1 + edge.intensity * 2}
                  strokeLinecap="round"
                  strokeDasharray="4 14"
                  opacity={0.55}
                >
                  {!reducedMotion ? (
                    <animate
                      attributeName="stroke-dashoffset"
                      from="18"
                      to="0"
                      dur={`${Math.max(0.5, 2.6 - edge.intensity * 2)}s`}
                      repeatCount="indefinite"
                    />
                  ) : null}
                </line>
              ) : null}
            </g>
          )
        })}
      </g>

      {/* Assets. Each is a button so the plan is operable from the keyboard (§91). */}
      <g>
        {nodes.map((node) => {
          const asset = assetByCode.get(node.code)
          const radius = (RADIUS[node.shape] ?? RADIUS.box) * Math.max(0.6, node.scale)
          const dim = highlighted && !highlighted.has(node.code)
          const selected = node.code === selectedCode

          return (
            <g
              key={node.code}
              opacity={dim ? 0.2 : 1}
              tabIndex={0}
              role="button"
              aria-label={`${node.code}${asset ? `, ${asset.state}, load ${Math.round(asset.loadPct)} per cent` : ''}`}
              onClick={() => onSelect(node.code)}
              onKeyDown={(event) => {
                if (event.key === 'Enter' || event.key === ' ') {
                  event.preventDefault()
                  onSelect(node.code)
                }
              }}
              className="cursor-pointer outline-none focus-visible:opacity-100"
              data-asset={node.code}
            >
              {selected ? (
                <circle cx={node.x} cy={node.z} r={radius * 1.6} fill="none" stroke="#ffffff" strokeWidth={2} opacity={0.7} />
              ) : null}
              <circle
                cx={node.x}
                cy={node.z}
                r={radius}
                fill={colourFor(layer, asset)}
                stroke="#060a11"
                strokeWidth={1.5}
              />
              {/* Loading ring: a second, non-colour channel for the same reading. */}
              {asset && !asset.isOutage ? (
                <circle
                  cx={node.x}
                  cy={node.z}
                  r={radius + 3.5}
                  fill="none"
                  stroke={colourFor('load', asset)}
                  strokeWidth={2.5}
                  strokeDasharray={`${Math.min(1, Math.abs(asset.loadPct) / 120) * 2 * Math.PI * (radius + 3.5)} ${2 * Math.PI * (radius + 3.5)}`}
                  transform={`rotate(-90 ${node.x} ${node.z})`}
                  opacity={0.9}
                />
              ) : null}
              {showLabels ? (
                <text
                  x={node.x}
                  y={node.z - radius - 7}
                  textAnchor="middle"
                  fill="#c7d6e8"
                  style={{ fontSize: Math.max(9, width / 90), fontFamily: 'ui-monospace, monospace' }}
                >
                  {node.code}
                  {asset ? ` · ${Math.round(asset.loadPct)}%` : ''}
                </text>
              ) : null}
            </g>
          )
        })}
      </g>
    </svg>
  )
}

/** The notice shown above the plan when it is standing in for the 3D scene. */
export function FallbackNotice({ reason, className }: { reason: string; className?: string }) {
  return (
    <div
      className={cn(
        'rounded-[--radius-panel] border border-watch/25 bg-watch/8 px-3 py-2 text-[11px] leading-relaxed text-watch',
        className,
      )}
    >
      {reason}
    </div>
  )
}
