'use client'

import { useMemo } from 'react'

import { useI18n } from '@/components/providers/i18n-provider'
import { cn } from '@/lib/cn'
import { STATE_STYLES } from '@/lib/ui/state-styles'
import type { GridState } from '@/lib/domain/enums'

/**
 * The twin, drawn as an isometric scene in SVG (§72 and the default renderer).
 *
 * This is not the WebGL fallback grudgingly provided — it is the primary view, and 3-D
 * is the enhancement. The reasoning is practical: an isometric projection of extruded
 * blocks reads as depth, costs nothing to start, renders identically on every machine in
 * a judging room, prints, and can be screen-read. A scene that takes two seconds to
 * initialise a GPU context is the wrong trade for a control room where the first frame
 * matters more than the fiftieth.
 *
 * Everything is derived from the same state objects the rest of the console uses, so a
 * unit that reads 91 % here reads 91 % in the table beside it.
 */

export interface SceneNode {
  code: string
  name: string
  nameAr: string
  assetTypeKey: string
  shape: string
  x: number
  y: number
  z: number
  scale: number
  capacityMw: number
  isFocus: boolean
}

export interface SceneEdge {
  from: string
  to: string
  kind: string
  intensity: number
}

export interface SceneAssetState {
  code: string
  loadPct: number
  tempC: number
  riskScore: number
  state: string
  thermalStress: number
  isOutage?: boolean
}

export type TwinView = 'standard' | 'xray' | 'thermal' | 'risk'

export interface TwinLayers {
  assets: boolean
  flow: boolean
  risk: boolean
  sensors: boolean
  cascade: boolean
  labels: boolean
}

export interface PropagationStep {
  assetCode: string
  offsetMin: number
  outcome: string
  probability: number
}

export interface SensorMarker {
  sensorCode: string
  assetCode: string
  channel: string
  value: number
  unit: string
  trust: number
  trend: 'rising' | 'falling' | 'steady'
}

const VIEW_W = 1000
const VIEW_H = 620
/** Isometric basis: 30° from horizontal, which is the classic engineering projection. */
const COS30 = Math.cos(Math.PI / 6)
const SIN30 = Math.sin(Math.PI / 6)

/** Footprint of each archetype in scene units, before the node's own scale. */
const FOOTPRINT: Record<string, { w: number; d: number; h: number }> = {
  // Wider than tall, because switchyard plant is: a transformer is a box on a pad, not a
  // tower. Earlier proportions read as crystals rather than as equipment.
  transformer: { w: 16, d: 14, h: 9 },
  substation: { w: 26, d: 20, h: 7 },
  line: { w: 5, d: 5, h: 22 },
  plant: { w: 28, d: 22, h: 12 },
  solar: { w: 26, d: 18, h: 3 },
  wind: { w: 4, d: 4, h: 26 },
  battery: { w: 20, d: 14, h: 6 },
  load: { w: 20, d: 16, h: 8 },
  ev: { w: 14, d: 12, h: 5 },
  building: { w: 16, d: 14, h: 8 },
  busbar: { w: 34, d: 4, h: 2 },
  box: { w: 14, d: 12, h: 7 },
}

/** Thermal ramp: cool grey through amber to the critical red. */
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 stateColour(state: string): string {
  return STATE_STYLES[(state as GridState) in STATE_STYLES ? (state as GridState) : 'normal'].hex
}

/** Darken a hex colour by a fraction, for the shaded faces of a block. */
function shade(hex: string, amount: number): string {
  const value = hex.replace('#', '')
  const num = Number.parseInt(value.length === 3 ? value.replace(/(.)/g, '$1$1') : value, 16)
  const r = Math.round(((num >> 16) & 255) * (1 - amount))
  const g = Math.round(((num >> 8) & 255) * (1 - amount))
  const b = Math.round((num & 255) * (1 - amount))
  return `rgb(${r} ${g} ${b})`
}

export function TwinScene2D({
  nodes,
  edges,
  assets,
  view = 'standard',
  layers,
  selectedCode,
  onSelect,
  propagation,
  sensors,
  className,
}: {
  nodes: SceneNode[]
  edges: SceneEdge[]
  assets: SceneAssetState[]
  view?: TwinView
  layers: TwinLayers
  selectedCode?: string | null
  onSelect?: (code: string) => void
  propagation?: PropagationStep[]
  sensors?: SensorMarker[]
  className?: string
}) {
  const { t, locale, n } = useI18n()
  const ar = locale === 'ar'

  const stateByCode = useMemo(
    () => new Map(assets.map((asset) => [asset.code, asset])),
    [assets],
  )
  const propagationByCode = useMemo(
    () => new Map((propagation ?? []).map((step) => [step.assetCode, step])),
    [propagation],
  )

  /**
   * Project the scene into the viewBox.
   *
   * Fit is computed from the nodes themselves rather than assumed, so a substation yard
   * and a national backbone both fill the frame without per-level tuning.
   */
  const projected = useMemo(() => {
    if (nodes.length === 0) return { points: [], scale: 1, offsetX: 0, offsetY: 0 }

    const raw = nodes.map((node) => ({
      node,
      ix: (node.x - node.z) * COS30,
      iy: (node.x + node.z) * SIN30 - node.y * 0.9,
    }))

    const minX = Math.min(...raw.map((entry) => entry.ix))
    const maxX = Math.max(...raw.map((entry) => entry.ix))
    const minY = Math.min(...raw.map((entry) => entry.iy))
    const maxY = Math.max(...raw.map((entry) => entry.iy))

    const padding = 90
    const scale = Math.min(
      (VIEW_W - padding * 2) / Math.max(maxX - minX, 1),
      (VIEW_H - padding * 2) / Math.max(maxY - minY, 1),
      // Never magnify past a sensible block size: a two-node scene should not fill the
      // frame with two enormous boxes.
      5.5,
    )
    const offsetX = VIEW_W / 2 - ((minX + maxX) / 2) * scale
    const offsetY = VIEW_H / 2 - ((minY + maxY) / 2) * scale

    return {
      points: raw.map((entry) => ({
        node: entry.node,
        sx: entry.ix * scale + offsetX,
        sy: entry.iy * scale + offsetY,
      })),
      scale,
      offsetX,
      offsetY,
    }
  }, [nodes])

  const positionByCode = useMemo(
    () => new Map(projected.points.map((point) => [point.node.code, point])),
    [projected],
  )

  // Painter's algorithm: draw back to front so blocks occlude correctly.
  const ordered = useMemo(
    () => [...projected.points].sort((a, b) => a.sy - b.sy),
    [projected],
  )

  const colourFor = (node: SceneNode) => {
    const state = stateByCode.get(node.code)
    if (!state) return '#3f5a80'
    if (view === 'thermal') return thermalColour(state.thermalStress)
    if (view === 'risk') return riskColour(state.riskScore)
    if (view === 'xray') return '#38bdf8'
    return stateColour(state.state)
  }

  if (nodes.length === 0) {
    return (
      <div className={cn('flex h-64 items-center justify-center text-xs text-text-muted', className)}>
        {t('uiState.empty')}
      </div>
    )
  }

  return (
    <svg
      viewBox={`0 0 ${VIEW_W} ${VIEW_H}`}
      className={cn('h-auto w-full select-none', className)}
      role="img"
      aria-label={t('twin.title')}
    >
      <defs>
        <linearGradient id="twin-ground" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%" stopColor="#0b1220" stopOpacity="0.0" />
          <stop offset="100%" stopColor="#0b1220" stopOpacity="0.55" />
        </linearGradient>
        <radialGradient id="twin-focus-glow">
          <stop offset="0%" stopColor="#24d07f" stopOpacity="0.35" />
          <stop offset="100%" stopColor="#24d07f" stopOpacity="0" />
        </radialGradient>
      </defs>

      <rect width={VIEW_W} height={VIEW_H} fill="url(#twin-ground)" />

      {/* Ground lattice: a reference plane so the blocks read as standing on something. */}
      {view !== 'xray' ? (
        <g opacity="0.16" stroke="currentColor" className="text-border-strong" strokeWidth="0.6">
          {Array.from({ length: 15 }, (_, index) => {
            const t = index / 14
            // Clamped to the viewBox: geometry outside it is clipped when drawn but is
            // still reported in layout measurements, which made the page report a
            // horizontal overflow it did not visually have.
            const skewX = 260
            const skewY = 180
            return (
              <g key={`grid-${index}`}>
                <line
                  x1={Math.min(VIEW_W, t * VIEW_W + skewX)}
                  y1={0}
                  x2={Math.max(0, t * VIEW_W)}
                  y2={VIEW_H}
                />
                <line
                  x1={0}
                  y1={Math.min(VIEW_H, t * VIEW_H + skewY)}
                  x2={VIEW_W}
                  y2={Math.max(0, t * VIEW_H)}
                />
              </g>
            )
          })}
        </g>
      ) : null}

      {/* ── Connections ─────────────────────────────────────────────────── */}
      {layers.flow || view === 'xray'
        ? edges.map((edge, index) => {
            const from = positionByCode.get(edge.from)
            const to = positionByCode.get(edge.to)
            if (!from || !to) return null

            const fromState = stateByCode.get(edge.from)
            const toState = stateByCode.get(edge.to)
            const stressed = Math.max(fromState?.riskScore ?? 0, toState?.riskScore ?? 0)
            const disconnected = fromState?.isOutage || toState?.isOutage
            const colour = disconnected
              ? '#64789a'
              : stressed >= 78
                ? STATE_STYLES.critical.hex
                : stressed >= 55
                  ? STATE_STYLES.warning.hex
                  : '#2b7d5c'

            // Thickness carries load; the dash animation carries direction of flow.
            const width = 1.4 + edge.intensity * 4
            return (
              <g key={`${edge.from}-${edge.to}-${index}`}>
                <line
                  x1={from.sx}
                  y1={from.sy}
                  x2={to.sx}
                  y2={to.sy}
                  stroke={colour}
                  strokeOpacity={disconnected ? 0.35 : 0.7}
                  strokeWidth={width}
                  strokeLinecap="round"
                />
                {!disconnected && layers.flow ? (
                  <line
                    x1={from.sx}
                    y1={from.sy}
                    x2={to.sx}
                    y2={to.sy}
                    stroke={colour}
                    strokeWidth={width * 0.8}
                    strokeLinecap="round"
                    className={cn(
                      'nabdh-flow',
                      stressed >= 78 ? 'nabdh-flow-fast' : stressed < 40 ? 'nabdh-flow-slow' : '',
                    )}
                  />
                ) : null}
              </g>
            )
          })
        : null}

      {/* ── Propagation wave (§26) ──────────────────────────────────────── */}
      {layers.cascade && propagation
        ? propagation.map((step, index) => {
            const point = positionByCode.get(step.assetCode)
            if (!point) return null
            return (
              <circle
                key={`wave-${step.assetCode}-${index}`}
                cx={point.sx}
                cy={point.sy}
                r={6}
                fill="none"
                stroke={step.outcome === 'failed' ? STATE_STYLES.critical.hex : STATE_STYLES.watch.hex}
                strokeWidth="2"
                className="nabdh-wave"
                style={{ animationDelay: `${Math.min(step.offsetMin, 60) * 0.03}s` }}
              />
            )
          })
        : null}

      {/* ── Blocks ──────────────────────────────────────────────────────── */}
      {ordered.map(({ node, sx, sy }) => {
        const state = stateByCode.get(node.code)
        const footprint = FOOTPRINT[node.shape] ?? FOOTPRINT.box
        const s = projected.scale * node.scale * 0.5
        const a = (footprint.w * s) / 2
        const b = (footprint.d * s) / 2
        const h = footprint.h * s * (view === 'xray' ? 0.5 : 1)

        // Isometric corners of the footprint, in screen space. A box's top face is
        // 1.73 times wider than it is tall in this projection; deriving the corners from
        // the (x, z) half-extents is what makes that fall out correctly. Halving the
        // horizontal term instead — an easy slip — produces diamonds standing on a point.
        const cx = (x: number, z: number) => sx + (x - z) * COS30
        const cy = (x: number, z: number) => sy + (x + z) * SIN30

        const corners = [
          [a, -b],
          [a, b],
          [-a, b],
          [-a, -b],
        ] as const
        const ground = corners.map(([x, z]) => [cx(x, z), cy(x, z)] as const)
        const roof = ground.map(([px, py]) => [px, py - h] as const)
        const halfW = (a + b) * COS30
        const topY = (a + b) * SIN30

        const colour = colourFor(node)
        const selected = node.code === selectedCode
        const propagated = propagationByCode.get(node.code)
        const dim = view === 'xray' && !selected && !propagated

        const poly = (points: ReadonlyArray<readonly [number, number]>) =>
          points.map(([px, py]) => `${px},${py}`).join(' ')

        // Top face, then the two visible sides, shaded so the form reads without
        // lighting: this is a diagram of a machine, not a render of one.
        const top = poly(roof)
        const right = poly([roof[0], roof[1], ground[1], ground[0]])
        const left = poly([roof[1], roof[2], ground[2], ground[1]])

        return (
          <g
            key={node.code}
            className={cn(onSelect && 'cursor-pointer')}
            opacity={dim ? 0.28 : state?.isOutage ? 0.45 : 1}
            onClick={() => onSelect?.(node.code)}
            role={onSelect ? 'button' : undefined}
            tabIndex={onSelect ? 0 : undefined}
            onKeyDown={(event) => {
              if (onSelect && (event.key === 'Enter' || event.key === ' ')) {
                event.preventDefault()
                onSelect(node.code)
              }
            }}
            aria-label={`${ar ? node.nameAr : node.name}${
              state ? ` — ${n(state.loadPct, { maximumFractionDigits: 0 })}%` : ''
            }`}
          >
            {node.isFocus || selected ? (
              <ellipse cx={sx} cy={sy + topY * 0.4} rx={halfW * 1.8} ry={topY * 1.8} fill="url(#twin-focus-glow)" />
            ) : null}

            {/* Contact shadow: without it the blocks float above the reference plane. */}
            {view !== 'xray' ? (
              <ellipse
                cx={sx}
                cy={sy + topY * 0.35}
                rx={halfW * 1.05}
                ry={topY * 0.75}
                fill="#02060c"
                opacity={0.45}
              />
            ) : null}

            <polygon points={left} fill={shade(colour, 0.5)} stroke={shade(colour, 0.65)} strokeWidth="0.5" />
            <polygon points={right} fill={shade(colour, 0.28)} stroke={shade(colour, 0.5)} strokeWidth="0.5" />
            <polygon
              points={top}
              fill={colour}
              fillOpacity={view === 'xray' ? 0.25 : 0.92}
              stroke={selected ? '#ffffff' : shade(colour, 0.3)}
              strokeWidth={selected ? 2 : 0.6}
            />

            {/* Load column: how full the unit is, drawn on the block itself so the
                reading does not depend on a legend. */}
            {/* Loading, as a short gauge above the unit. Deliberately capped: a taller
                bar for a worse reading turns a switchyard into a bar chart. */}
            {layers.assets && state && !state.isOutage && view !== 'xray' ? (
              <>
                <rect
                  x={sx - halfW * 0.55}
                  y={sy - h - topY - 7}
                  width={halfW * 1.1}
                  height={2.6}
                  rx={1.3}
                  fill="#0b1220"
                  opacity={0.75}
                />
                <rect
                  x={sx - halfW * 0.55}
                  y={sy - h - topY - 7}
                  width={halfW * 1.1 * Math.min(Math.abs(state.loadPct), 130) / 130}
                  height={2.6}
                  rx={1.3}
                  fill={riskColour(state.riskScore)}
                />
              </>
            ) : null}

            {layers.labels && (node.isFocus || selected || nodes.length <= 26) ? (
              <text
                x={sx}
                y={sy + topY + 12}
                textAnchor="middle"
                className="pointer-events-none fill-current text-[9px] text-text-muted"
              >
                {node.code}
              </text>
            ) : null}
          </g>
        )
      })}

      {/* ── Sensor overlay (§18) ────────────────────────────────────────── */}
      {layers.sensors && sensors
        ? sensors.slice(0, 16).map((sensor, index) => {
            const point = positionByCode.get(sensor.assetCode)
            if (!point) return null
            const angle = (index / Math.max(1, sensors.length)) * Math.PI * 2
            const cx = point.sx + Math.cos(angle) * 42
            const cy = point.sy - 26 + Math.sin(angle) * 22

            return (
              <g key={sensor.sensorCode}>
                <line
                  x1={point.sx}
                  y1={point.sy - 8}
                  x2={cx}
                  y2={cy + 6}
                  stroke="currentColor"
                  className="text-info"
                  strokeWidth="0.6"
                  strokeOpacity="0.5"
                />
                <rect
                  x={cx - 30}
                  y={cy - 9}
                  width={60}
                  height={18}
                  rx={4}
                  fill="#0b1220"
                  fillOpacity="0.9"
                  stroke={sensor.trust >= 80 ? '#38bdf8' : sensor.trust >= 60 ? '#fbbf24' : '#ef4444'}
                  strokeWidth="0.8"
                />
                <text
                  x={cx}
                  y={cy + 3}
                  textAnchor="middle"
                  className="pointer-events-none fill-current font-mono text-[8px] text-text"
                >
                  {n(sensor.value, { maximumFractionDigits: 1 })}
                  {sensor.unit}
                  {sensor.trend === 'rising' ? ' ↑' : sensor.trend === 'falling' ? ' ↓' : ''}
                </text>
              </g>
            )
          })
        : null}
    </svg>
  )
}
