<script setup>
import * as THREE from 'three'

const props = defineProps({
  active: { type: Number, default: -1 },
  total: { type: Number, default: 5 },
  delivered: { type: Boolean, default: false },
  failure: { type: Boolean, default: false }
})

const container = ref(null)

let renderer = null
let scene = null
let camera = null
let curve = null
let truck = null
let driver = null
let helper = null
let recipient = null
let parcel = null
let parcelOnGround = null
let warehouse = null
let house = null
let sun = null
let happySpot = null
let sunRayGroup = null
const flagClothes = []
let animId = 0
let resizeObs = null
let lastTime = 0

let currentT = 0
let targetT = 0
let prevT = 0
let wheelAngle = 0

// Camera framing
const defaultCamPos = new THREE.Vector3()
const defaultLookAt = new THREE.Vector3(0, 0.25, 0)
const zoomedCamPos = new THREE.Vector3()
const zoomedLookAt = new THREE.Vector3()
let zoomFactor = 0
const ZOOM_SPEED = 0.5

// Delivery state machine
let deliveryState = 'idle'
let deliveryStartedAt = 0

const SPEED_T_PER_SEC = 0.4
const WHEEL_SPIN_PER_T = 55

// Slightly shorter road so the buildings fit cleanly outside the road extents
const ROAD_HALF = 2.3
const REQUIRED_HALF_W = 3.7
const REQUIRED_HALF_H = 1.0 // taller view so house roof + sun are in frame

const FOV_Y_DEG = 38

const ORANGE = 0xeb531e
const GREEN = 0x26a65b
const RED = 0xd9534f
const GRAY = 0xcfd4d9
const NAVY = 0x192e47
const SUN_YELLOW = 0xfff2a8
const SUN_CORE = 0xffe066

const computeT = () => {
  if (props.active < 0) return 0
  return Math.max(0, Math.min(1, props.active / Math.max(1, props.total - 1)))
}

const colorFor = (i, active) => {
  if (props.failure && i === active) return RED
  if (props.delivered && i === props.total - 1) return GREEN
  if (i <= active) return ORANGE
  return GRAY
}

// ---------- Truck ----------
const buildTruck = () => {
  const group = new THREE.Group()
  const cabMat = new THREE.MeshStandardMaterial({ color: ORANGE, roughness: 0.35, metalness: 0.2 })
  const bodyMat = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.4 })
  const tireMat = new THREE.MeshStandardMaterial({ color: 0x1a1a1a, roughness: 0.7 })
  const windowMat = new THREE.MeshStandardMaterial({ color: 0x87c5ff, roughness: 0.2, metalness: 0.5 })
  const lightMat = new THREE.MeshStandardMaterial({ color: 0xffe19a, emissive: 0xffd17d, emissiveIntensity: 0.7 })
  const doorMat = new THREE.MeshStandardMaterial({ color: 0xeeeeee, roughness: 0.4 })

  const trailer = new THREE.Mesh(new THREE.BoxGeometry(0.7, 0.5, 0.45), bodyMat)
  trailer.position.set(-0.18, 0.32, 0)
  group.add(trailer)
  const stripe = new THREE.Mesh(new THREE.BoxGeometry(0.71, 0.08, 0.46), cabMat)
  stripe.position.set(-0.18, 0.42, 0)
  group.add(stripe)

  const rearDoor = new THREE.Mesh(new THREE.BoxGeometry(0.02, 0.4, 0.4), doorMat)
  rearDoor.position.set(-0.535, 0.32, 0)
  group.add(rearDoor)

  const cab = new THREE.Mesh(new THREE.BoxGeometry(0.36, 0.36, 0.42), cabMat)
  cab.position.set(0.32, 0.27, 0)
  group.add(cab)
  const win = new THREE.Mesh(new THREE.BoxGeometry(0.28, 0.18, 0.4), windowMat)
  win.position.set(0.36, 0.36, 0)
  group.add(win)

  const hl1 = new THREE.Mesh(new THREE.BoxGeometry(0.05, 0.06, 0.08), lightMat)
  hl1.position.set(0.52, 0.18, 0.16)
  group.add(hl1)
  const hl2 = hl1.clone()
  hl2.position.z = -0.16
  group.add(hl2)

  const wheelGeo = new THREE.CylinderGeometry(0.1, 0.1, 0.06, 18)
  wheelGeo.rotateX(Math.PI / 2)
  const wheels = []
  for (const x of [-0.42, -0.05, 0.32]) {
    for (const z of [-0.24, 0.24]) {
      const w = new THREE.Mesh(wheelGeo, tireMat)
      w.position.set(x, 0.1, z)
      group.add(w)
      wheels.push(w)
    }
  }

  group.userData = { wheels, rearDoor }
  return group
}

// ---------- Person ----------
const buildPerson = ({ shirt, pants, cap, skin = 0xfdd9b5 }) => {
  const group = new THREE.Group()
  const skinMat = new THREE.MeshStandardMaterial({ color: skin, roughness: 0.6 })
  const shirtMat = new THREE.MeshStandardMaterial({ color: shirt, roughness: 0.5 })
  const pantsMat = new THREE.MeshStandardMaterial({ color: pants, roughness: 0.5 })
  const capMat = new THREE.MeshStandardMaterial({ color: cap, roughness: 0.5 })

  const head = new THREE.Mesh(new THREE.SphereGeometry(0.06, 16, 12), skinMat)
  head.position.y = 0.36
  group.add(head)

  const capMesh = new THREE.Mesh(
    new THREE.SphereGeometry(0.063, 16, 12, 0, Math.PI * 2, 0, Math.PI / 2),
    capMat
  )
  capMesh.position.y = 0.36
  group.add(capMesh)

  // Cap brim points along +X (forward)
  const brim = new THREE.Mesh(new THREE.BoxGeometry(0.09, 0.012, 0.05), capMat)
  brim.position.set(0.05, 0.37, 0)
  group.add(brim)

  const torso = new THREE.Mesh(new THREE.BoxGeometry(0.1, 0.18, 0.06), shirtMat)
  torso.position.y = 0.21
  group.add(torso)

  const armL = new THREE.Group()
  armL.position.set(-0.065, 0.29, 0)
  group.add(armL)
  const armLMesh = new THREE.Mesh(new THREE.BoxGeometry(0.035, 0.16, 0.035), skinMat)
  armLMesh.position.y = -0.08
  armL.add(armLMesh)

  const armR = new THREE.Group()
  armR.position.set(0.065, 0.29, 0)
  group.add(armR)
  const armRMesh = new THREE.Mesh(new THREE.BoxGeometry(0.035, 0.16, 0.035), skinMat)
  armRMesh.position.y = -0.08
  armR.add(armRMesh)

  const legL = new THREE.Group()
  legL.position.set(-0.028, 0.12, 0)
  group.add(legL)
  const legLMesh = new THREE.Mesh(new THREE.BoxGeometry(0.04, 0.14, 0.04), pantsMat)
  legLMesh.position.y = -0.07
  legL.add(legLMesh)

  const legR = new THREE.Group()
  legR.position.set(0.028, 0.12, 0)
  group.add(legR)
  const legRMesh = new THREE.Mesh(new THREE.BoxGeometry(0.04, 0.14, 0.04), pantsMat)
  legRMesh.position.y = -0.07
  legR.add(legRMesh)

  group.userData = { armL, armR, legL, legR }
  group.visible = false
  return group
}

// ---------- Parcel ----------
const buildParcel = () => {
  const g = new THREE.Group()
  const box = new THREE.Mesh(
    new THREE.BoxGeometry(0.11, 0.09, 0.09),
    new THREE.MeshStandardMaterial({ color: 0xb8895c, roughness: 0.85 })
  )
  g.add(box)
  const tape = new THREE.Mesh(
    new THREE.BoxGeometry(0.024, 0.092, 0.092),
    new THREE.MeshStandardMaterial({ color: 0xeacf99, roughness: 0.55 })
  )
  g.add(tape)
  g.visible = false
  return g
}

// ---------- Warehouse ----------
const buildWarehouse = () => {
  const g = new THREE.Group()
  const wallMat = new THREE.MeshStandardMaterial({ color: 0xcfd9e3, roughness: 0.75 })
  const roofMat = new THREE.MeshStandardMaterial({ color: 0x5e6b7c, roughness: 0.7 })
  const doorMat = new THREE.MeshStandardMaterial({ color: 0x3d4753, roughness: 0.55 })
  const accentMat = new THREE.MeshStandardMaterial({ color: ORANGE, roughness: 0.45 })
  const winMat = new THREE.MeshStandardMaterial({ color: 0x87c5ff, emissive: 0x4a8fdf, emissiveIntensity: 0.2, roughness: 0.3 })

  const walls = new THREE.Mesh(new THREE.BoxGeometry(1.0, 0.78, 0.6), wallMat)
  walls.position.y = 0.39
  g.add(walls)

  const roof = new THREE.Mesh(new THREE.BoxGeometry(1.08, 0.07, 0.68), roofMat)
  roof.position.y = 0.81
  g.add(roof)

  const stripe = new THREE.Mesh(new THREE.BoxGeometry(1.05, 0.06, 0.04), accentMat)
  stripe.position.set(0, 0.78, 0.32)
  g.add(stripe)

  const door = new THREE.Mesh(new THREE.BoxGeometry(0.36, 0.48, 0.02), doorMat)
  door.position.set(0, 0.24, 0.301)
  g.add(door)
  for (let i = 0; i < 5; i++) {
    const slat = new THREE.Mesh(new THREE.BoxGeometry(0.36, 0.012, 0.025), accentMat)
    slat.position.set(0, 0.07 + i * 0.095, 0.313)
    g.add(slat)
  }

  const winStrip = new THREE.Mesh(new THREE.BoxGeometry(0.42, 0.09, 0.025), winMat)
  winStrip.position.set(-0.28, 0.62, 0.301)
  g.add(winStrip)
  const winStrip2 = winStrip.clone()
  winStrip2.position.x = 0.28
  g.add(winStrip2)

  return g
}

// ---------- House ----------
const buildHouse = () => {
  const g = new THREE.Group()
  const wallMat = new THREE.MeshStandardMaterial({ color: 0xf5e9d4, roughness: 0.7 })
  const roofMat = new THREE.MeshStandardMaterial({ color: 0x9a3a1f, roughness: 0.65 })
  const doorMat = new THREE.MeshStandardMaterial({ color: 0x6b4226, roughness: 0.55 })
  const winMat = new THREE.MeshStandardMaterial({ color: 0x87c5ff, emissive: 0x4a8fdf, emissiveIntensity: 0.25, roughness: 0.3 })
  const trimMat = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.5 })

  // Taller walls so the truck doesn't sit over the roof line
  const WALL_W = 0.78
  const WALL_H = 0.68
  const WALL_D = 0.6
  const walls = new THREE.Mesh(new THREE.BoxGeometry(WALL_W, WALL_H, WALL_D), wallMat)
  walls.position.y = WALL_H / 2
  g.add(walls)

  // Pitched roof — triangle extruded along +Z. No Y-rotation needed:
  // the gables naturally end up facing ±Z (front / back toward the camera),
  // the sloped faces face ±X. Previously the roof was rotated AND positioned,
  // which compounded into a visible displacement.
  const OVERHANG = 0.05
  const ROOF_DEPTH = WALL_D + OVERHANG * 2
  const ROOF_HALF_W = WALL_W / 2 + OVERHANG
  const ROOF_HEIGHT = 0.32

  const roofShape = new THREE.Shape()
  roofShape.moveTo(-ROOF_HALF_W, 0)
  roofShape.lineTo(ROOF_HALF_W, 0)
  roofShape.lineTo(0, ROOF_HEIGHT)
  roofShape.closePath()
  const roofGeo = new THREE.ExtrudeGeometry(roofShape, { depth: ROOF_DEPTH, bevelEnabled: false })
  // Center the extrusion around Z=0 so the roof sits symmetrically over the walls
  roofGeo.translate(0, 0, -ROOF_DEPTH / 2)
  const roof = new THREE.Mesh(roofGeo, roofMat)
  roof.position.set(0, WALL_H, 0)
  g.add(roof)

  // Door (front face, +Z)
  const door = new THREE.Mesh(new THREE.BoxGeometry(0.18, 0.32, 0.02), doorMat)
  door.position.set(-0.18, 0.16, WALL_D / 2 + 0.001)
  g.add(door)
  const knob = new THREE.Mesh(
    new THREE.SphereGeometry(0.014, 8, 8),
    new THREE.MeshStandardMaterial({ color: 0xd9a44a, metalness: 0.6, roughness: 0.3 })
  )
  knob.position.set(-0.11, 0.18, WALL_D / 2 + 0.012)
  g.add(knob)

  // Window with white cross trim
  const win = new THREE.Mesh(new THREE.BoxGeometry(0.2, 0.18, 0.02), winMat)
  win.position.set(0.18, 0.42, WALL_D / 2 + 0.001)
  g.add(win)
  const winV = new THREE.Mesh(new THREE.BoxGeometry(0.015, 0.18, 0.025), trimMat)
  winV.position.set(0.18, 0.42, WALL_D / 2 + 0.01)
  g.add(winV)
  const winH = new THREE.Mesh(new THREE.BoxGeometry(0.2, 0.015, 0.025), trimMat)
  winH.position.set(0.18, 0.42, WALL_D / 2 + 0.01)
  g.add(winH)

  // Chimney on the roof, offset to the back-right for a more "house" feel
  const chimney = new THREE.Mesh(
    new THREE.BoxGeometry(0.08, 0.2, 0.08),
    new THREE.MeshStandardMaterial({ color: 0x8a4a3a, roughness: 0.7 })
  )
  chimney.position.set(0.22, WALL_H + 0.18, -0.05)
  g.add(chimney)

  g.userData = {
    doorOffset: new THREE.Vector3(-0.18, 0, WALL_D / 2 + 0.08)
  }
  return g
}

// ---------- Sun ----------
const buildSun = () => {
  const g = new THREE.Group()
  // Bright core
  const core = new THREE.Mesh(
    new THREE.SphereGeometry(0.22, 24, 24),
    new THREE.MeshBasicMaterial({ color: SUN_CORE })
  )
  g.add(core)
  // Soft outer glow
  const glow = new THREE.Mesh(
    new THREE.SphereGeometry(0.34, 24, 24),
    new THREE.MeshBasicMaterial({ color: SUN_YELLOW, transparent: true, opacity: 0.45 })
  )
  g.add(glow)
  // Larger ambient halo
  const halo = new THREE.Mesh(
    new THREE.SphereGeometry(0.48, 24, 24),
    new THREE.MeshBasicMaterial({ color: SUN_YELLOW, transparent: true, opacity: 0.18 })
  )
  g.add(halo)
  // Rays as flat planes
  const rayMat = new THREE.MeshBasicMaterial({ color: SUN_YELLOW, transparent: true, opacity: 0.35 })
  for (let i = 0; i < 8; i++) {
    const ray = new THREE.Mesh(new THREE.PlaneGeometry(0.7, 0.05), rayMat)
    ray.rotation.z = (i / 8) * Math.PI * 2
    g.add(ray)
  }
  return g
}

// ---------- Flag ----------
const buildFlag = (colorHex) => {
  const g = new THREE.Group()
  const base = new THREE.Mesh(
    new THREE.CylinderGeometry(0.09, 0.11, 0.04, 16),
    new THREE.MeshStandardMaterial({ color: GRAY, roughness: 0.6 })
  )
  base.position.y = 0.02
  g.add(base)
  const pole = new THREE.Mesh(
    new THREE.CylinderGeometry(0.026, 0.026, 0.6, 8),
    new THREE.MeshStandardMaterial({ color: 0x7a8594, roughness: 0.5 })
  )
  pole.position.y = 0.3
  g.add(pole)
  const cloth = new THREE.Mesh(
    new THREE.BoxGeometry(0.28, 0.18, 0.02),
    new THREE.MeshStandardMaterial({ color: colorHex, roughness: 0.5 })
  )
  cloth.position.set(0.16, 0.5, 0)
  g.add(cloth)
  return { group: g, cloth }
}

// ---------- Camera ----------
const fitCamera = () => {
  const el = container.value
  if (!el || !camera) return
  const w = Math.max(1, el.clientWidth)
  const h = Math.max(1, el.clientHeight)
  const aspect = w / h

  const fovY = THREE.MathUtils.degToRad(FOV_Y_DEG)
  const fovX = 2 * Math.atan(Math.tan(fovY / 2) * aspect)

  const distForW = REQUIRED_HALF_W / Math.tan(fovX / 2)
  const distForH = REQUIRED_HALF_H / Math.tan(fovY / 2)
  // Extra margin because the camera sits slightly off-axis (3/4 view) so the
  // far side of the scene mustn't get cropped.
  const dist = Math.max(distForW, distForH) * 1.2 + 0.4

  // Three-quarter angle: camera offset slightly to the right and higher up.
  // Gives a more cinematic perspective than a flat frontal view.
  defaultCamPos.set(dist * 0.18, dist * 0.28, dist * 0.95)
  defaultLookAt.set(0, 0.35, 0)

  camera.aspect = aspect
  camera.fov = FOV_Y_DEG
  camera.updateProjectionMatrix()
}

const easeInOut = (t) => t * t * (3 - 2 * t)

const applyCamera = () => {
  if (!camera) return
  const t = zoomFactor <= 0 ? 0 : zoomFactor >= 1 ? 1 : easeInOut(zoomFactor)
  camera.position.set(
    defaultCamPos.x + (zoomedCamPos.x - defaultCamPos.x) * t,
    defaultCamPos.y + (zoomedCamPos.y - defaultCamPos.y) * t,
    defaultCamPos.z + (zoomedCamPos.z - defaultCamPos.z) * t
  )
  camera.lookAt(
    defaultLookAt.x + (zoomedLookAt.x - defaultLookAt.x) * t,
    defaultLookAt.y + (zoomedLookAt.y - defaultLookAt.y) * t,
    defaultLookAt.z + (zoomedLookAt.z - defaultLookAt.z) * t
  )
}

// ---------- Setup ----------
const setup = () => {
  const el = container.value
  if (!el) return
  const w = Math.max(1, el.clientWidth)
  const h = Math.max(1, el.clientHeight)

  renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
  renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2))
  renderer.setSize(w, h, false)
  renderer.domElement.style.display = 'block'
  renderer.domElement.style.width = '100%'
  renderer.domElement.style.height = '100%'
  renderer.outputColorSpace = THREE.SRGBColorSpace
  el.appendChild(renderer.domElement)

  scene = new THREE.Scene()

  camera = new THREE.PerspectiveCamera(FOV_Y_DEG, w / h, 0.1, 100)
  fitCamera()
  applyCamera()

  // Sunny lighting — warm ambient + bright sun directional + soft fill
  scene.add(new THREE.AmbientLight(0xfff2d8, 0.82))
  const sunLight = new THREE.DirectionalLight(0xfff0c2, 1.25)
  sunLight.position.set(3.5, 5, 2)
  scene.add(sunLight)
  const fill = new THREE.DirectionalLight(0xc5dfff, 0.4)
  fill.position.set(-3, 3, 3)
  scene.add(fill)

  // Sun in the upper-right back area so it shows through the canvas
  sun = buildSun()
  sun.position.set(2.6, 2.1, -0.8)
  sunRayGroup = new THREE.Group()
  // Extra "shafts of light" streaming down from the sun — hidden until the
  // delivery sequence ends, then they fade in to produce the "sun coming in
  // and shining" moment.
  const shaftMat = new THREE.MeshBasicMaterial({ color: 0xfff2a8, transparent: true, opacity: 0, side: THREE.DoubleSide, depthWrite: false })
  for (let i = 0; i < 5; i++) {
    const shaft = new THREE.Mesh(new THREE.PlaneGeometry(0.12, 3.5), shaftMat.clone())
    shaft.position.copy(sun.position)
    shaft.position.x -= 0.2 * i
    shaft.position.y -= 1.2
    shaft.rotation.z = 0.2 + i * 0.06
    sunRayGroup.add(shaft)
  }
  scene.add(sunRayGroup)

  // Spotlight that "turns on" during the happy moment — lit warmly from the
  // sun's direction toward the recipient.
  happySpot = new THREE.SpotLight(0xfff2b8, 0, 12, Math.PI * 0.22, 0.45, 1)
  happySpot.position.copy(sun.position)
  scene.add(happySpot)
  scene.add(happySpot.target)

  // Soft warm-tinted ground plane
  const ground = new THREE.Mesh(
    new THREE.PlaneGeometry(24, 6),
    new THREE.MeshStandardMaterial({ color: 0xf6efe1, roughness: 0.92, transparent: true, opacity: 0.7 })
  )
  ground.rotation.x = -Math.PI / 2
  ground.position.y = -0.001
  scene.add(ground)

  // Curve along X with gentle S-shape
  curve = new THREE.CatmullRomCurve3([
    new THREE.Vector3(-ROAD_HALF, 0, 0.18),
    new THREE.Vector3(-ROAD_HALF * 0.5, 0.04, -0.16),
    new THREE.Vector3(0, 0.06, 0.18),
    new THREE.Vector3(ROAD_HALF * 0.5, 0.04, -0.16),
    new THREE.Vector3(ROAD_HALF, 0, 0.18)
  ])

  const roadGeo = new THREE.TubeGeometry(curve, 120, 0.075, 12, false)
  const roadMat = new THREE.MeshStandardMaterial({
    color: ORANGE,
    roughness: 0.55,
    emissive: 0x2a1005,
    emissiveIntensity: 0.18
  })
  scene.add(new THREE.Mesh(roadGeo, roadMat))

  const dashPoints = curve.getPoints(80).map(p => new THREE.Vector3(p.x, p.y + 0.012, p.z))
  const dashGeo = new THREE.BufferGeometry().setFromPoints(dashPoints)
  const dashMat = new THREE.LineDashedMaterial({ color: 0xffffff, dashSize: 0.16, gapSize: 0.1 })
  const dash = new THREE.Line(dashGeo, dashMat)
  dash.computeLineDistances()
  scene.add(dash)

  const activeIdx = props.active
  const startPoint = curve.getPointAt(0)
  const endPoint = curve.getPointAt(0.999)
  for (let i = 0; i < props.total; i++) {
    const t = i / Math.max(1, props.total - 1)
    const p = curve.getPointAt(t)
    const { group, cloth } = buildFlag(colorFor(i, activeIdx))
    group.position.copy(p)
    group.position.z += 0.34
    scene.add(group)
    flagClothes.push(cloth)
  }

  // Buildings — placed OUTSIDE the road's X extents so neither the road nor
  // flags can obscure them.
  warehouse = buildWarehouse()
  warehouse.position.set(startPoint.x - 0.7, 0, 0.05)
  scene.add(warehouse)

  house = buildHouse()
  house.position.set(endPoint.x + 0.65, 0, 0.05)
  scene.add(house)

  // Pre-compute the zoomed-in camera target — frames the parked truck, the
  // last flag, the house, and all three people. Slight 3/4 angle matches the
  // default view so the zoom doesn't feel like a cut.
  zoomedCamPos.set(endPoint.x + 0.1, 0.8, endPoint.z + 1.95)
  zoomedLookAt.set(endPoint.x + 0.55, 0.4, endPoint.z + 0.35)

  truck = buildTruck()
  scene.add(truck)

  driver = buildPerson({ shirt: ORANGE, pants: NAVY, cap: NAVY })
  helper = buildPerson({ shirt: 0xffffff, pants: NAVY, cap: ORANGE })
  recipient = buildPerson({ shirt: 0xffd45a, pants: 0x3a4a66, cap: 0xb83b1a })
  scene.add(driver)
  scene.add(helper)
  scene.add(recipient)

  parcel = buildParcel()
  scene.add(parcel)
  parcelOnGround = buildParcel()
  parcelOnGround.visible = false
  scene.add(parcelOnGround)

  currentT = 0
  prevT = 0
  targetT = computeT()
  deliveryState = 'idle'
  zoomFactor = 0

  resizeObs = new ResizeObserver(() => {
    if (!renderer || !el) return
    const W = Math.max(1, el.clientWidth)
    const H = Math.max(1, el.clientHeight)
    renderer.setSize(W, H, false)
    fitCamera()
  })
  resizeObs.observe(el)

  lastTime = performance.now()
  animate()
}

// ---------- Walk helpers ----------
const lerpVec3 = (out, a, b, t) => {
  out.x = a.x + (b.x - a.x) * t
  out.y = a.y + (b.y - a.y) * t
  out.z = a.z + (b.z - a.z) * t
  return out
}

const setPersonWalk = (person, walkPhase, intensity = 1) => {
  const swing = Math.sin(walkPhase) * 0.7 * intensity
  const swingArm = Math.sin(walkPhase) * 0.5 * intensity
  person.userData.legL.rotation.x = swing
  person.userData.legR.rotation.x = -swing
  person.userData.armL.rotation.x = -swingArm
  person.userData.armR.rotation.x = swingArm
}
const setPersonRest = (person) => {
  person.userData.legL.rotation.x = 0
  person.userData.legR.rotation.x = 0
  person.userData.armL.rotation.x = 0
  person.userData.armR.rotation.x = 0
}

// ---------- Animation loop ----------
const animate = () => {
  animId = requestAnimationFrame(animate)
  const now = performance.now()
  const dt = Math.min(0.05, (now - lastTime) / 1000)
  lastTime = now

  // Travel — slow down on the last 8% for smoother arrival
  const remaining = Math.abs(targetT - currentT)
  const arrivalFactor = remaining < 0.08 ? Math.max(0.25, remaining / 0.08) : 1
  const step = SPEED_T_PER_SEC * dt * arrivalFactor
  if (remaining > 0.0008) {
    currentT += Math.sign(targetT - currentT) * Math.min(step, remaining)
  } else {
    currentT = targetT
  }

  // Wheels
  const tDelta = currentT - prevT
  prevT = currentT
  wheelAngle += tDelta * WHEEL_SPIN_PER_T
  if (truck?.userData?.wheels) {
    for (const w of truck.userData.wheels) w.rotation.z = -wheelAngle
  }

  // Truck transform
  if (truck) {
    if (props.active < 0) {
      truck.visible = false
    } else {
      truck.visible = true
      const t = Math.max(0.001, Math.min(0.999, currentT))
      const pos = curve.getPointAt(t)
      const tan = curve.getTangentAt(t)
      truck.position.copy(pos)
      const isMoving = Math.abs(tDelta) > 0.0001
      truck.position.y += 0.02 + (isMoving ? Math.sin(now / 220) * 0.012 : 0)
      truck.rotation.y = Math.atan2(-tan.z, tan.x)
    }
  }

  // Sun gentle rotation + idle shine
  if (sun) {
    sun.rotation.z = Math.sin(now / 1500) * 0.08
    sun.children.forEach((c, i) => {
      if (c.material?.opacity !== undefined && i > 2) {
        c.material.opacity = 0.32 + Math.sin(now / 380 + i) * 0.08
      }
    })
  }

  // Delivery sequence
  const finalPos = curve.getPointAt(0.999)
  const truckParked = props.delivered && Math.abs(targetT - currentT) < 0.003

  if (!props.delivered) {
    deliveryState = 'idle'
    if (driver) driver.visible = false
    if (helper) helper.visible = false
    if (recipient) recipient.visible = false
    if (parcel) parcel.visible = false
    if (parcelOnGround) parcelOnGround.visible = false
    if (truck?.userData?.rearDoor) truck.userData.rearDoor.rotation.y = 0
  } else if (truckParked && deliveryState === 'idle') {
    deliveryState = 'parked'
    deliveryStartedAt = now
  }

  if (deliveryState !== 'idle' && truck && finalPos && curve) {
    const elapsed = (now - deliveryStartedAt) / 1000

    truck.visible = true

    // Door swings open between 0.2..0.7s
    if (truck.userData?.rearDoor) {
      const doorT = Math.max(0, Math.min(1, (elapsed - 0.2) / 0.5))
      truck.userData.rearDoor.rotation.y = easeInOut(doorT) * (Math.PI * 0.55)
    }

    const flagPos = new THREE.Vector3(finalPos.x, 0, finalPos.z + 0.34)
    const truckSide = new THREE.Vector3(finalPos.x + 0.05, 0, finalPos.z + 0.45)
    const truckBack = new THREE.Vector3(finalPos.x - 0.55, 0, finalPos.z + 0.3)

    // ---- DRIVER ----
    const driverEmergeStart = 0.3
    const driverWalkStart = 0.8
    const driverWalkEnd = 2.4
    if (elapsed > driverEmergeStart) {
      driver.visible = true
      if (elapsed < driverWalkStart) {
        const t = easeInOut(Math.min(1, (elapsed - driverEmergeStart) / (driverWalkStart - driverEmergeStart)))
        const start = new THREE.Vector3(finalPos.x + 0.35, 0, finalPos.z)
        lerpVec3(driver.position, start, truckSide, t)
        driver.rotation.y = Math.PI * 0.5
        setPersonRest(driver)
      } else if (elapsed < driverWalkEnd) {
        const t = easeInOut(Math.min(1, (elapsed - driverWalkStart) / (driverWalkEnd - driverWalkStart)))
        const target = new THREE.Vector3(flagPos.x - 0.22, 0, flagPos.z + 0.05)
        lerpVec3(driver.position, truckSide, target, t)
        driver.rotation.y = Math.PI * 0.5
        setPersonWalk(driver, elapsed * 12, 1)
      } else {
        driver.position.set(flagPos.x - 0.22, 0, flagPos.z + 0.05)
        driver.rotation.y = Math.PI * 0.5
        setPersonRest(driver)
        // Friendly idle wave
        driver.userData.armR.rotation.x = -1.4 + Math.sin(now / 250) * 0.18
      }
    } else {
      driver.visible = false
    }

    // ---- HELPER (carries parcel out) ----
    const helperEmergeStart = 0.8
    const helperWalkStart = 1.4
    const helperPlaceAt = 3.0
    if (elapsed > helperEmergeStart) {
      helper.visible = true
      parcel.visible = elapsed < helperPlaceAt

      if (elapsed < helperWalkStart) {
        const t = easeInOut(Math.min(1, (elapsed - helperEmergeStart) / (helperWalkStart - helperEmergeStart)))
        const start = new THREE.Vector3(finalPos.x - 0.45, 0, finalPos.z)
        lerpVec3(helper.position, start, truckBack, t)
        helper.rotation.y = Math.PI * 0.5
        setPersonRest(helper)
        helper.userData.armL.rotation.x = -1.3
        helper.userData.armR.rotation.x = -1.3
      } else if (elapsed < helperPlaceAt) {
        const t = easeInOut(Math.min(1, (elapsed - helperWalkStart) / (helperPlaceAt - helperWalkStart)))
        const target = new THREE.Vector3(flagPos.x + 0.18, 0, flagPos.z + 0.05)
        lerpVec3(helper.position, truckBack, target, t)
        helper.rotation.y = Math.PI * 0.5
        setPersonWalk(helper, elapsed * 12, 0.55)
        helper.userData.armL.rotation.x = -1.3
        helper.userData.armR.rotation.x = -1.3
        // Carried parcel sits in front of chest
        parcel.position.set(helper.position.x, 0.22, helper.position.z + 0.1)
      } else {
        helper.position.set(flagPos.x + 0.18, 0, flagPos.z + 0.05)
        helper.rotation.y = Math.PI * 0.5
        setPersonRest(helper)
      }
    } else {
      helper.visible = false
      parcel.visible = false
    }

    // ---- PARCEL placed at flag base ----
    if (elapsed > helperPlaceAt) {
      parcelOnGround.visible = true
      parcelOnGround.position.set(flagPos.x, 0.05, flagPos.z + 0.18)
      const settleT = Math.max(0, Math.min(1, (elapsed - helperPlaceAt) / 0.4))
      parcelOnGround.position.y = 0.05 + (1 - easeInOut(settleT)) * 0.18
    }

    // ---- RECIPIENT (comes out of the house, picks up parcel, gets happy) ----
    const recipEmergeStart = 3.4
    const recipWalkStart = 4.0
    const recipPickUpAt = 5.6
    const recipHoldFrom = 6.0

    // Anchor positions in world space — use the house's own door offset so
    // the recipient spawns right at the door.
    const doorOff = house?.userData?.doorOffset
    const houseDoor = new THREE.Vector3(
      (house?.position?.x ?? finalPos.x + 0.65) + (doorOff?.x ?? -0.18),
      0,
      (house?.position?.z ?? 0.05) + (doorOff?.z ?? 0.38)
    )
    const parcelPos = new THREE.Vector3(flagPos.x, 0, flagPos.z + 0.18)

    if (elapsed > recipEmergeStart) {
      recipient.visible = true

      if (elapsed < recipWalkStart) {
        // Step out from the door
        const t = easeInOut(Math.min(1, (elapsed - recipEmergeStart) / (recipWalkStart - recipEmergeStart)))
        const start = new THREE.Vector3(houseDoor.x + 0.05, 0, houseDoor.z - 0.15) // just inside the door
        lerpVec3(recipient.position, start, houseDoor, t)
        // Face -X (toward the parcel/road area)
        recipient.rotation.y = -Math.PI * 0.5
        setPersonRest(recipient)
      } else if (elapsed < recipPickUpAt) {
        // Walk from the door to the parcel
        const t = (elapsed - recipWalkStart) / (recipPickUpAt - recipWalkStart)
        const tt = easeInOut(Math.min(1, t))
        const target = new THREE.Vector3(parcelPos.x + 0.18, 0, parcelPos.z + 0.05)
        lerpVec3(recipient.position, houseDoor, target, tt)
        // Aim toward the parcel
        const dx = parcelPos.x - recipient.position.x
        const dz = parcelPos.z - recipient.position.z
        recipient.rotation.y = Math.atan2(-dz, dx)
        setPersonWalk(recipient, elapsed * 13, 1)
      } else if (elapsed < recipHoldFrom) {
        // Bend / reach down to pick up parcel
        recipient.position.set(parcelPos.x + 0.18, 0, parcelPos.z + 0.05)
        recipient.rotation.y = -Math.PI * 0.5
        const reachT = Math.min(1, (elapsed - recipPickUpAt) / 0.4)
        const reach = easeInOut(reachT)
        recipient.userData.armL.rotation.x = -0.9 - reach * 0.6
        recipient.userData.armR.rotation.x = -0.9 - reach * 0.6
        recipient.userData.legL.rotation.x = reach * 0.3
        recipient.userData.legR.rotation.x = reach * 0.3
        // Parcel rises into hands
        parcelOnGround.visible = reachT < 1
        if (reachT >= 1) {
          // Attach parcel to recipient's hands
          parcel.visible = true
          parcel.position.set(recipient.position.x - 0.05, 0.22, recipient.position.z)
        }
      } else {
        // HAPPY: the recipient stands calmly holding the parcel up while the
        // sun shines in — no jumping. A very subtle arm sway keeps it alive.
        recipient.position.set(parcelPos.x + 0.18, 0, parcelPos.z + 0.05)
        recipient.rotation.y = -Math.PI * 0.5
        recipient.userData.armL.rotation.x = -1.55 // parcel held high
        recipient.userData.armR.rotation.x = -1.05 + Math.sin(now / 380) * 0.18 // gentle sway
        recipient.userData.legL.rotation.x = 0
        recipient.userData.legR.rotation.x = 0
        parcel.visible = true
        parcel.position.set(
          recipient.position.x - 0.05,
          0.24,
          recipient.position.z + 0.02
        )
        parcelOnGround.visible = false
      }
    } else {
      recipient.visible = false
    }
  }

  // Flag wave
  flagClothes.forEach((c, i) => {
    c.rotation.y = Math.sin(now / 450 + i * 0.7) * 0.18
  })

  // Camera zoom
  const wantZoom = deliveryState !== 'idle' && props.delivered ? 1 : 0
  const dz = ZOOM_SPEED * dt
  if (zoomFactor < wantZoom) zoomFactor = Math.min(wantZoom, zoomFactor + dz)
  else if (zoomFactor > wantZoom) zoomFactor = Math.max(wantZoom, zoomFactor - dz)
  applyCamera()

  // Sunshine during the happy moment: ease up the sun rays and the spotlight
  // once the recipient is standing with the parcel.
  const happyActive = recipient?.visible && deliveryState !== 'idle'
    && ((now - deliveryStartedAt) / 1000) > 6.0
  if (happySpot) {
    const targetI = happyActive ? 1.8 : 0
    happySpot.intensity += (targetI - happySpot.intensity) * Math.min(1, dt * 2.2)
    if (recipient) {
      happySpot.target.position.set(recipient.position.x, 0.3, recipient.position.z)
      happySpot.target.updateMatrixWorld()
    }
  }
  if (sunRayGroup) {
    const targetOpacity = happyActive ? 0.38 : 0
    sunRayGroup.children.forEach((s, i) => {
      const o = s.material.opacity + (targetOpacity - s.material.opacity) * Math.min(1, dt * 1.6)
      s.material.opacity = o + (happyActive ? Math.sin(now / 400 + i) * 0.06 : 0)
    })
  }

  renderer.render(scene, camera)
}

watch(() => [props.active, props.total, props.delivered, props.failure], () => {
  if (!scene) return
  targetT = computeT()
  deliveryState = 'idle'
  const activeIdx = props.active
  flagClothes.forEach((c, i) => {
    c.material.color.setHex(colorFor(i, activeIdx))
  })
})

onMounted(() => {
  if (import.meta.client) setup()
})

onBeforeUnmount(() => {
  if (animId) cancelAnimationFrame(animId)
  if (resizeObs) resizeObs.disconnect()
  scene?.traverse((obj) => {
    if (obj.geometry) obj.geometry.dispose?.()
    if (obj.material) {
      if (Array.isArray(obj.material)) obj.material.forEach(m => m.dispose?.())
      else obj.material.dispose?.()
    }
  })
  if (renderer) {
    renderer.dispose()
    renderer.domElement?.remove()
  }
})
</script>

<template>
  <div ref="container" class="journey-3d" aria-hidden="true" />
</template>

<style scoped>
.journey-3d {
  width: 100%;
  height: 280px;
  position: relative;
  border-radius: 14px;
  /* Sky-tinted gradient — sets the "sunny day" mood behind the canvas */
  background:
    radial-gradient(ellipse at 80% 0%, rgba(255, 224, 130, .42), transparent 55%),
    linear-gradient(180deg, rgba(170, 215, 255, .35) 0%, rgba(245, 233, 212, .25) 70%, transparent 100%);
  overflow: hidden;
}
@media (max-width: 900px) { .journey-3d { height: 230px; } }
@media (max-width: 640px) { .journey-3d { height: 200px; } }
</style>
