import React, { useRef, useEffect, Suspense } from 'react'
import { Canvas, useFrame } from '@react-three/fiber'
import { Environment, SoftShadows } from '@react-three/drei'
import * as THREE from 'three'
import { Model as SceneModel } from '../models/Scene3D'
import CameraRig from './CameraRig'
import TruckAnimation from './TruckAnimation'
import StreetLights from './StreetLights'

const dayBgColor = new THREE.Color('#f5f5f7')
const tempColor = new THREE.Color()
const _truckPos = new THREE.Vector3()

/**
 * Per-tier Canvas / lighting / feature settings. One table so the desktop ↔
 * tablet ↔ mobile trade-offs are visible and tuned in a single place.
 *
 *   shadows      — WebGL shadow map on at all? (mobile: off entirely)
 *   softShadows  — drei PCSS soft shadows (desktop only; expensive)
 *   environment  — drei "city" HDR (network + GPU; dropped on mobile)
 *   streetLights — extra spotLights (currently visually off → desktop only)
 *   dpr          — device-pixel-ratio clamp
 *   antialias    — MSAA (desktop only; costly on mobile fill rate)
 *   shadowMap    — directional-light shadow resolution
 */
const TIER = {
  desktop: { shadows: true, softShadows: true, environment: true, streetLights: true, dpr: [1, 1.5], antialias: true, shadowMap: 2048 },
  tablet: { shadows: true, softShadows: false, environment: true, streetLights: false, dpr: [1, 1.5], antialias: false, shadowMap: 1024 },
  mobile: { shadows: false, softShadows: false, environment: false, streetLights: false, dpr: [1, 1], antialias: false, shadowMap: 512 },
}

// Directional "sun" that keeps its shadow frustum centred on the moving truck.
// PERF: reads the truck position transiently in useFrame — no React state.
const SceneLighting = React.memo(function SceneLighting({ truckRef, shadows, shadowMap }) {
  const dirLightRef = useRef()
  const targetRef = useRef()

  useEffect(() => {
    if (dirLightRef.current && targetRef.current) {
      dirLightRef.current.target = targetRef.current
    }
  }, [])

  useFrame((state) => {
    // Track the truck so the (small) shadow frustum always covers it.
    if (dirLightRef.current && targetRef.current && truckRef.current) {
      truckRef.current.getWorldPosition(_truckPos)
      targetRef.current.position.copy(_truckPos)
      targetRef.current.updateMatrixWorld()
      dirLightRef.current.position.set(_truckPos.x + 10, _truckPos.y + 20, _truckPos.z + 10)
    }
    // Keep the daytime background colour stable (day→night transition is disabled).
    if (state.scene) {
      state.scene.background = tempColor.copy(dayBgColor)
      state.scene.environmentIntensity = 1.0
    }
  })

  return (
    <group>
      <ambientLight intensity={0.45} />
      <directionalLight
        ref={dirLightRef}
        castShadow={shadows}
        position={[10, 20, 10]}
        intensity={1.5}
        shadow-mapSize-width={shadowMap}
        shadow-mapSize-height={shadowMap}
        shadow-camera-far={100}
        shadow-camera-left={-35}
        shadow-camera-right={35}
        shadow-camera-top={35}
        shadow-camera-bottom={-35}
        shadow-bias={-0.0001}
      />
      <object3D ref={targetRef} />
    </group>
  )
})

/**
 * Fires `onReady` once the GLB has resolved (this only mounts inside the Suspense
 * boundary, after the model loaded and Scene3D's useLayoutEffect wired the refs)
 * AND the first real frame has painted (double rAF = one composited frame). This
 * is the deterministic "scene ready" signal — no timeouts.
 */
function SceneReadySignal({ onReady }) {
  useEffect(() => {
    let r1 = 0
    let r2 = 0
    r1 = requestAnimationFrame(() => {
      r2 = requestAnimationFrame(() => onReady?.())
    })
    return () => {
      cancelAnimationFrame(r1)
      cancelAnimationFrame(r2)
    }
  }, [onReady])
  return null
}

function Experience({ dashboardRefs, wheelRefs, truckRef, tier = 'desktop', onReady }) {
  const cfg = TIER[tier] ?? TIER.desktop

  return (
    <div style={{ width: '100%', height: '100%', position: 'absolute', top: 0, left: 0 }}>
      <Canvas
        // Mobile turns the shadow map off entirely; desktop/tablet keep it but
        // only the truck casts and only the ground receives (see Scene3D pruning).
        shadows={cfg.shadows}
        // Cap DPR so a retina phone doesn't render this heavy scene into a 2–3×
        // framebuffer (GPU memory blowup → context loss).
        dpr={cfg.dpr}
        camera={{ position: [32, 12, -18], fov: 45 }}
        gl={{ antialias: cfg.antialias, powerPreference: 'high-performance' }}
      >
        <color attach="background" args={['#f5f5f7']} />

        {/* Soft (PCSS) shadows on desktop only — they multiply shadow-map cost. */}
        {cfg.softShadows && <SoftShadows size={10} samples={12} focus={1.0} />}

        <SceneLighting truckRef={truckRef} shadows={cfg.shadows} shadowMap={cfg.shadowMap} />

        {/* Decorative street spotlights (visually off in the day scene) — desktop only. */}
        {cfg.streetLights && <StreetLights />}

        {/* Image-based lighting in its OWN boundary. The "city" HDR is an
            external fetch; keeping it separate means scene readiness is gated on
            the (local, fast) GLB only — the HDR can pop in a beat later without
            delaying the loader→ready handoff. Mobile skips it for a cheap fill. */}
        {cfg.environment ? (
          <Suspense fallback={null}>
            <Environment preset="city" />
          </Suspense>
        ) : (
          <hemisphereLight args={['#ffffff', '#9aa0a6', 0.9]} />
        )}

        {/* Main scene + readiness signal. SceneReadySignal mounts only once the
            GLB has resolved (same boundary) → deterministic "ready". */}
        <Suspense fallback={null}>
          <SceneModel dashboardRefs={dashboardRefs} truckRef={truckRef} wheelRefs={wheelRefs} tier={tier} />
          <SceneReadySignal onReady={onReady} />
        </Suspense>

        <TruckAnimation truckRef={truckRef} wheelRefs={wheelRefs} />
        <CameraRig />

        {/* Post-processing intentionally omitted (EffectComposer + StrictMode +
            ssr:false interaction; see git history). Mobile would disable it anyway. */}
      </Canvas>
    </div>
  )
}

export default React.memo(Experience)
