'use client'

import { useMemo, useRef } from 'react'
import { Canvas, useFrame, type ThreeEvent } from '@react-three/fiber'
import { OrbitControls } from '@react-three/drei'
import * as THREE from 'three'

import { STATE_STYLES } from '@/lib/ui/state-styles'
import type { GridState } from '@/lib/domain/enums'
import type { SceneAssetState, SceneEdge, SceneNode, TwinView } from './twin-scene-2d'

/**
 * The twin in three dimensions (§15, §16).
 *
 * Loaded only when the browser offers WebGL and the operator asks for it, because it is
 * the expensive option and the isometric SVG scene is a complete substitute. The model is
 * deliberately schematic — boxes, cylinders and a ground plane — since the goal is
 * operational legibility rather than photorealism: an engineer needs to see which unit is
 * hot and what it feeds, not what its paint looks like.
 *
 * Performance choices worth naming: one shared geometry per archetype rather than one per
 * node, materials cached by colour so a scene of 260 members allocates a handful rather
 * than hundreds, and a render loop that only runs when something is moving.
 */

const COLOUR_CACHE = new Map<string, THREE.MeshStandardMaterial>()

function materialFor(colour: string, opacity: number): THREE.MeshStandardMaterial {
  const key = `${colour}-${opacity}`
  const cached = COLOUR_CACHE.get(key)
  if (cached) return cached
  const material = new THREE.MeshStandardMaterial({
    color: colour,
    roughness: 0.55,
    metalness: 0.15,
    transparent: opacity < 1,
    opacity,
  })
  COLOUR_CACHE.set(key, material)
  return material
}

const FOOTPRINT: Record<string, [number, number, number]> = {
  transformer: [7, 9, 7],
  substation: [12, 6, 10],
  line: [3, 16, 3],
  plant: [13, 12, 11],
  solar: [14, 2, 9],
  wind: [2, 18, 2],
  battery: [10, 5, 7],
  load: [10, 7, 9],
  ev: [7, 5, 6],
  building: [8, 7, 7],
  busbar: [18, 2, 2],
  box: [7, 6, 7],
}

function thermalColour(stress: number): string {
  if (stress >= 0.85) return '#ef4444'
  if (stress >= 0.65) return '#f97316'
  if (stress >= 0.4) return '#fbbf24'
  if (stress >= 0.2) return '#38bdf8'
  return '#3f5a80'
}

function riskColour(risk: number): string {
  if (risk >= 78) return STATE_STYLES.critical.hex
  if (risk >= 60) return STATE_STYLES.warning.hex
  if (risk >= 40) return STATE_STYLES.watch.hex
  return STATE_STYLES.normal.hex
}

function colourFor(view: TwinView, state: SceneAssetState | undefined): string {
  if (!state) return '#3f5a80'
  if (view === 'thermal') return thermalColour(state.thermalStress)
  if (view === 'risk') return riskColour(state.riskScore)
  if (view === 'xray') return '#38bdf8'
  const key = (state.state as GridState) in STATE_STYLES ? (state.state as GridState) : 'normal'
  return STATE_STYLES[key].hex
}

/** A gentle bob on the focused unit, so the eye finds it without a label. */
function FocusRing({ position }: { position: [number, number, number] }) {
  const ref = useRef<THREE.Mesh>(null)
  useFrame(({ clock }) => {
    if (!ref.current) return
    const pulse = 1 + Math.sin(clock.elapsedTime * 2) * 0.08
    ref.current.scale.set(pulse, 1, pulse)
  })
  return (
    <mesh ref={ref} position={position} rotation={[-Math.PI / 2, 0, 0]}>
      <ringGeometry args={[7, 8.4, 48]} />
      <meshBasicMaterial color="#24d07f" transparent opacity={0.55} side={THREE.DoubleSide} />
    </mesh>
  )
}

function Block({
  node,
  state,
  view,
  selected,
  onSelect,
}: {
  node: SceneNode
  state: SceneAssetState | undefined
  view: TwinView
  selected: boolean
  onSelect?: (code: string) => void
}) {
  const [w, h, d] = FOOTPRINT[node.shape] ?? FOOTPRINT.box
  const scale = node.scale
  const height = h * scale * (view === 'xray' ? 0.5 : 1)
  const colour = colourFor(view, state)
  const opacity = view === 'xray' ? 0.35 : state?.isOutage ? 0.4 : 1

  const material = useMemo(() => materialFor(colour, opacity), [colour, opacity])

  const handleClick = (event: ThreeEvent<MouseEvent>) => {
    event.stopPropagation()
    onSelect?.(node.code)
  }

  return (
    <group position={[node.x, 0, node.z]}>
      <mesh
        position={[0, height / 2, 0]}
        material={material}
        onClick={handleClick}
        onPointerOver={(event) => {
          event.stopPropagation()
          document.body.style.cursor = 'pointer'
        }}
        onPointerOut={() => {
          document.body.style.cursor = ''
        }}
      >
        <boxGeometry args={[w * scale, height, d * scale]} />
      </mesh>

      {selected ? (
        <mesh position={[0, height / 2, 0]}>
          <boxGeometry args={[w * scale * 1.12, height * 1.08, d * scale * 1.12]} />
          <meshBasicMaterial color="#ffffff" wireframe transparent opacity={0.5} />
        </mesh>
      ) : null}

      {/* Loading column, the same reading the isometric view draws. */}
      {state && !state.isOutage && view !== 'xray' ? (
        <mesh position={[0, height + 1 + Math.min(Math.abs(state.loadPct), 130) * 0.04, 0]}>
          <boxGeometry args={[0.6, Math.min(Math.abs(state.loadPct), 130) * 0.08, 0.6]} />
          <meshBasicMaterial color={riskColour(state.riskScore)} />
        </mesh>
      ) : null}

      {node.isFocus ? <FocusRing position={[0, 0.3, 0]} /> : null}
    </group>
  )
}

function Connections({
  nodes,
  edges,
  assets,
}: {
  nodes: SceneNode[]
  edges: SceneEdge[]
  assets: SceneAssetState[]
}) {
  const stateByCode = useMemo(() => new Map(assets.map((asset) => [asset.code, asset])), [assets])
  const positions = useMemo(() => new Map(nodes.map((node) => [node.code, node])), [nodes])

  // One buffered line set per stress band rather than one object per edge: three draw
  // calls instead of two hundred and forty.
  const bands = useMemo(() => {
    const groups: Record<string, number[]> = { calm: [], stressed: [], critical: [] }
    for (const edge of edges) {
      const from = positions.get(edge.from)
      const to = positions.get(edge.to)
      if (!from || !to) continue
      const stress = Math.max(
        stateByCode.get(edge.from)?.riskScore ?? 0,
        stateByCode.get(edge.to)?.riskScore ?? 0,
      )
      const bucket = stress >= 78 ? 'critical' : stress >= 55 ? 'stressed' : 'calm'
      groups[bucket].push(from.x, from.y + 2, from.z, to.x, to.y + 2, to.z)
    }
    return groups
  }, [edges, positions, stateByCode])

  return (
    <>
      {(
        [
          ['calm', '#2b7d5c', 0.4],
          ['stressed', STATE_STYLES.warning.hex, 0.7],
          ['critical', STATE_STYLES.critical.hex, 0.9],
        ] as const
      ).map(([key, colour, opacity]) => {
        const points = bands[key]
        if (points.length === 0) return null
        const array = new Float32Array(points)
        return (
          <lineSegments key={key}>
            <bufferGeometry>
              <bufferAttribute attach="attributes-position" args={[array, 3]} />
            </bufferGeometry>
            <lineBasicMaterial color={colour} transparent opacity={opacity} />
          </lineSegments>
        )
      })}
    </>
  )
}

export default function TwinScene3D({
  nodes,
  edges,
  assets,
  view = 'standard',
  selectedCode,
  onSelect,
  radius,
  reducedMotion = false,
}: {
  nodes: SceneNode[]
  edges: SceneEdge[]
  assets: SceneAssetState[]
  view?: TwinView
  selectedCode?: string | null
  onSelect?: (code: string) => void
  radius: number
  reducedMotion?: boolean
}) {
  const stateByCode = useMemo(() => new Map(assets.map((asset) => [asset.code, asset])), [assets])
  const distance = Math.max(60, radius * 1.6)

  return (
    <Canvas
      // Only render when something changes unless an animation needs frames. On a wall
      // display left open for hours this is the difference between an idle GPU and a hot
      // one.
      frameloop={reducedMotion ? 'demand' : 'always'}
      dpr={[1, 1.75]}
      camera={{ position: [distance * 0.8, distance * 0.75, distance * 0.8], fov: 42, far: 6000 }}
      gl={{ antialias: true, powerPreference: 'high-performance' }}
      style={{ width: '100%', height: '100%' }}
    >
      <color attach="background" args={['#070b14']} />
      <fog attach="fog" args={['#070b14', distance * 1.4, distance * 4]} />

      <ambientLight intensity={0.55} />
      <directionalLight position={[60, 90, 40]} intensity={1.1} color="#dfe9ff" />
      <directionalLight position={[-70, 40, -50]} intensity={0.35} color="#38bdf8" />

      <gridHelper args={[radius * 4, 28, '#1e2d47', '#111c2e']} position={[0, -0.05, 0]} />

      <Connections nodes={nodes} edges={edges} assets={assets} />

      {nodes.map((node) => (
        <Block
          key={node.code}
          node={node}
          state={stateByCode.get(node.code)}
          view={view}
          selected={node.code === selectedCode}
          onSelect={onSelect}
        />
      ))}

      <OrbitControls
        makeDefault
        enablePan
        enableDamping
        dampingFactor={0.08}
        minDistance={distance * 0.25}
        maxDistance={distance * 3}
        // Stop the camera dropping below the ground plane, which is disorienting and
        // shows the underside of a scene that has no underside.
        maxPolarAngle={Math.PI / 2.15}
      />
    </Canvas>
  )
}
