/* eslint-disable */
/* =========================================================
   TEO IoT Dashboard — Three.js low-poly room digital twin.
   Style: low-poly architectural interiors (think poly.pizza
   bedroom / art-gallery aesthetic) rendered in TEO palette.
   - Full-height exterior walls
   - Interior walls between zones with doorway openings
   - Per-zone floor tint + interior equipment props
     (transformers, battery stacks, server racks, conveyors,
     pallet stacks)
   - Devices float as glowing markers on stems, on top of
     equipment, with mesh lines to gateways
   - Camera defaults to a cinematic isometric showing
     interior depth; orbit to inspect any zone
   ========================================================= */

// Per-site layouts. Each zone has a kind (drives interior props)
// and (optionally) doorOpenings: which walls should have a gap.
//   doors: array of 'N'|'S'|'E'|'W' — walls along that side render with a gap.
const ZONE_LAYOUTS_3D = {
  // Manchester precision facility — 4 zones in a row
  'MFG-MAN-01': [
    { name:'CNC cells',   kind:'cnc',      x:-36, z:0, w:24, d:22, doors:['E'] },
    { name:'Assembly',    kind:'assembly', x:-12, z:0, w:24, d:22, doors:['E','W'] },
    { name:'Paint booth', kind:'paint',    x:12,  z:0, w:24, d:22, doors:['E','W'] },
    { name:'Plant room',  kind:'plant',    x:36,  z:0, w:24, d:22, doors:['W'] },
  ],
  // Park Plaza London — Samsung HVAC stack
  'HOT-LDN-01': [
    { name:'Rooftop plant',         kind:'vrf-outdoor', x:-36, z:0, w:24, d:22, doors:['E'] },
    { name:'Plant room',            kind:'chiller',     x:-12, z:0, w:24, d:22, doors:['E','W'] },
    { name:'Floor 14 · guest rooms',kind:'guest-rooms', x:12,  z:0, w:24, d:22, doors:['E','W'] },
    { name:'Atrium & restaurant',   kind:'ahu-room',    x:36,  z:0, w:24, d:22, doors:['W'] },
  ],
  // MMU All Saints — campus spaces
  'MMU-MAN-01': [
    { name:'Lecture theatre', kind:'lecture',  x:-36, z:0, w:24, d:22, doors:['E'] },
    { name:'Classroom',       kind:'classroom',x:-12, z:0, w:24, d:22, doors:['E','W'] },
    { name:'Study space',     kind:'study',    x:12,  z:0, w:24, d:22, doors:['E','W'] },
    { name:'Faculty office',  kind:'office',   x:36,  z:0, w:24, d:22, doors:['W'] },
  ],
};

const STATE_HEX_3D = {
  ok:   0x3A6FF8,
  warn: 0xE0A13B,
  down: 0xC44A4A,
  idle: 0x878787,
};

// ----- Low-poly material palette (TEO-aligned but with subtle variation) -----
const PAL = {
  ground:    0x05080F,   // outside the building
  floor:     {           // per-zone floor tints
    transformer: 0x1A2240,
    battery:     0x162039,
    sensor:      0x111A33,
    logistics:   0x1B1E2E,
    sortation:   0x1E2233,
    // Manufacturing
    cnc:         0x141A2E,
    assembly:    0x18203A,
    paint:       0x1F2540,
    plant:       0x141B30,
    // HVAC / hotel
    'vrf-outdoor': 0x121830,
    chiller:       0x10172E,
    'guest-rooms': 0x1A1F38,
    'ahu-room':    0x161D34,
    // Campus
    lecture:   0x17203A,
    classroom: 0x1A2240,
    study:     0x161F38,
    office:    0x161D34,
    default:   0x131A30,
  },
  exterior:  0x1F2742,   // exterior walls
  interior:  0x2A325A,   // interior partition walls
  wallTrim:  0x3A6FF8,   // top trim accent
  prop:      {
    body:    0x2E3658,
    bodyDark:0x1E243F,
    metal:   0x4A5478,
    accent:  0x7A9CFF,
    pallet:  0x6A6052,   // muted tan — only off-palette element, for contrast
    belt:    0x1A1E2E,
  },
};

// =============================================================
// Helpers for building procedural geometry
// =============================================================

function makeWallSegments(THREE, side, x, z, length, height, thickness, openings, material) {
  // Build one wall (a single span) along `side` of a zone, with optional doorway gaps.
  // side: 'N' | 'S' (run along X axis) or 'E' | 'W' (run along Z axis)
  const group = new THREE.Group();
  const isNS = (side === 'N' || side === 'S');
  // Default: single full-length wall; if openings is true, split into 2 pieces with a 4-unit gap in the middle
  const segments = [];
  if (openings) {
    const gap = Math.min(5.5, length * 0.4);
    const seg = (length - gap) / 2;
    segments.push({ off: -length/2 + seg/2,           len: seg });
    segments.push({ off:  length/2 - seg/2,           len: seg });
  } else {
    segments.push({ off: 0, len: length });
  }
  segments.forEach(s => {
    const geo = isNS
      ? new THREE.BoxGeometry(s.len, height, thickness)
      : new THREE.BoxGeometry(thickness, height, s.len);
    const mesh = new THREE.Mesh(geo, material);
    if (isNS) mesh.position.set(x + s.off, height/2, z);
    else      mesh.position.set(x, height/2, z + s.off);
    group.add(mesh);
    // Top trim (thin coloured strip on top edge — TEO accent)
    const trimGeo = isNS
      ? new THREE.BoxGeometry(s.len + 0.02, 0.05, thickness + 0.02)
      : new THREE.BoxGeometry(thickness + 0.02, 0.05, s.len + 0.02);
    const trim = new THREE.Mesh(
      trimGeo,
      new THREE.MeshStandardMaterial({ color:PAL.wallTrim, emissive:PAL.wallTrim, emissiveIntensity:0.4, roughness:0.6 })
    );
    if (isNS) trim.position.set(x + s.off, height + 0.025, z);
    else      trim.position.set(x, height + 0.025, z + s.off);
    group.add(trim);
  });
  return group;
}

// Place interior props for a zone based on kind. Returns a Group.
function makeZoneProps(THREE, zone) {
  const g = new THREE.Group();
  const matBody     = new THREE.MeshStandardMaterial({ color:PAL.prop.body, roughness:0.85 });
  const matBodyDark = new THREE.MeshStandardMaterial({ color:PAL.prop.bodyDark, roughness:0.9 });
  const matMetal    = new THREE.MeshStandardMaterial({ color:PAL.prop.metal, roughness:0.45, metalness:0.45 });
  const matAccent   = new THREE.MeshStandardMaterial({ color:PAL.prop.accent, emissive:PAL.prop.accent, emissiveIntensity:0.3 });
  const matPallet   = new THREE.MeshStandardMaterial({ color:PAL.prop.pallet, roughness:0.95 });
  const matBelt     = new THREE.MeshStandardMaterial({ color:PAL.prop.belt, roughness:0.95 });

  switch (zone.kind) {
    case 'transformer': {
      // Two big transformer cabinets with vents + a metal frame between them
      [-zone.w/4, zone.w/4].forEach((ox) => {
        const cab = new THREE.Mesh(new THREE.BoxGeometry(5.5, 3.6, 5.5), matBody);
        cab.position.set(zone.x + ox, 1.8, zone.z);
        g.add(cab);
        // top plate
        const top = new THREE.Mesh(new THREE.BoxGeometry(5.8, 0.18, 5.8), matMetal);
        top.position.set(zone.x + ox, 3.7, zone.z);
        g.add(top);
        // vent stripes (three thin slats)
        [-1.4, -0.2, 1].forEach(oy => {
          const vent = new THREE.Mesh(new THREE.BoxGeometry(4.0, 0.18, 0.08), matBodyDark);
          vent.position.set(zone.x + ox, 1.6 + oy + 1.2, zone.z + 2.78);
          g.add(vent);
        });
        // small warning light on top
        const light = new THREE.Mesh(new THREE.SphereGeometry(0.22, 12, 8), matAccent);
        light.position.set(zone.x + ox + 2, 3.95, zone.z + 1.8);
        g.add(light);
      });
      // floor cable tray
      const tray = new THREE.Mesh(new THREE.BoxGeometry(zone.w * 0.6, 0.08, 0.9), matMetal);
      tray.position.set(zone.x, 0.06, zone.z + zone.d * 0.3);
      g.add(tray);
      break;
    }
    case 'battery': {
      // Stacked battery cabinet wall along the back, plus inverter cabinet
      const totalW = zone.w * 0.7, back = zone.z - zone.d/2 + 1.5;
      const cabW = 1.4;
      const cabCount = Math.floor(totalW / (cabW + 0.15));
      for (let i = 0; i < cabCount; i++) {
        const x = zone.x - totalW/2 + i*(cabW + 0.15) + cabW/2;
        const cab = new THREE.Mesh(new THREE.BoxGeometry(cabW, 3.2, 1.2), matBody);
        cab.position.set(x, 1.6, back);
        g.add(cab);
        // status LED row
        const led = new THREE.Mesh(new THREE.BoxGeometry(cabW * 0.7, 0.06, 0.06), matAccent);
        led.position.set(x, 2.7, back + 0.62);
        g.add(led);
      }
      // Inverter unit
      const inv = new THREE.Mesh(new THREE.BoxGeometry(3.4, 2.6, 1.6), matBodyDark);
      inv.position.set(zone.x + zone.w*0.25, 1.3, zone.z + zone.d*0.25);
      g.add(inv);
      const invTop = new THREE.Mesh(new THREE.BoxGeometry(3.6, 0.12, 1.8), matMetal);
      invTop.position.set(zone.x + zone.w*0.25, 2.66, zone.z + zone.d*0.25);
      g.add(invTop);
      break;
    }
    case 'sensor': {
      // 2 rows of server racks
      [zone.z - 3, zone.z + 3].forEach((row, ri) => {
        for (let i = 0; i < 4; i++) {
          const x = zone.x - 6 + i*4;
          const rack = new THREE.Mesh(new THREE.BoxGeometry(2.4, 2.4, 1.4), matBody);
          rack.position.set(x, 1.2, row);
          g.add(rack);
          // front grille (4 thin lines)
          for (let j = 0; j < 4; j++) {
            const g1 = new THREE.Mesh(new THREE.BoxGeometry(2.0, 0.04, 0.04), matAccent);
            g1.material = new THREE.MeshStandardMaterial({ color:PAL.prop.accent, emissive:PAL.prop.accent, emissiveIntensity:0.25 });
            g1.position.set(x, 0.4 + j*0.5, row + (ri===0 ? 0.72 : -0.72));
            g.add(g1);
          }
        }
      });
      // central gateway pedestal
      const ped = new THREE.Mesh(new THREE.CylinderGeometry(0.7, 0.9, 0.6, 16), matMetal);
      ped.position.set(zone.x, 0.3, zone.z);
      g.add(ped);
      break;
    }
    case 'logistics': {
      // Pallet stacks scattered + simple forklift silhouette
      const palletPositions = [
        [-9, -6], [-9, -2], [-9, 2], [-9, 6],
        [-4, 6], [0, 6], [4, 6],
      ];
      palletPositions.forEach(([ox, oz]) => {
        // 3 stacked pallets
        for (let k = 0; k < 3; k++) {
          const p = new THREE.Mesh(new THREE.BoxGeometry(1.6, 0.18, 1.2), matPallet);
          p.position.set(zone.x + ox, 0.09 + k*0.22, zone.z + oz);
          g.add(p);
          // boxes on top of top pallet
          if (k === 2) {
            const box = new THREE.Mesh(new THREE.BoxGeometry(1.3, 0.9, 1.0), matBody);
            box.position.set(zone.x + ox, 0.55 + 0.45, zone.z + oz);
            g.add(box);
          }
        }
      });
      // forklift silhouette — body + mast
      const fk = new THREE.Group();
      const fkBody = new THREE.Mesh(new THREE.BoxGeometry(2.0, 1.0, 1.4), matAccent);
      fkBody.material = new THREE.MeshStandardMaterial({ color:0xE0A13B, roughness:0.6 });
      fkBody.position.set(0, 0.5, 0);
      const fkCab = new THREE.Mesh(new THREE.BoxGeometry(1.4, 1.2, 1.3), matMetal);
      fkCab.position.set(-0.4, 1.55, 0);
      const fkMast = new THREE.Mesh(new THREE.BoxGeometry(0.2, 2.6, 1.2), matMetal);
      fkMast.position.set(1.1, 1.3, 0);
      fk.add(fkBody, fkCab, fkMast);
      fk.position.set(zone.x + 6, 0, zone.z + 4);
      fk.rotation.y = Math.PI / 6;
      g.add(fk);
      break;
    }
    case 'sortation': {
      // Two conveyor belt segments running along X axis
      [-4, 4].forEach((oz) => {
        const beltLen = zone.w * 0.75;
        const belt = new THREE.Mesh(new THREE.BoxGeometry(beltLen, 0.18, 1.6), matBelt);
        belt.position.set(zone.x, 0.9, zone.z + oz);
        g.add(belt);
        // belt frame rails (sides)
        [-0.85, 0.85].forEach(or => {
          const rail = new THREE.Mesh(new THREE.BoxGeometry(beltLen, 0.6, 0.12), matMetal);
          rail.position.set(zone.x, 0.6, zone.z + oz + or);
          g.add(rail);
        });
        // legs
        [-beltLen/2 + 1, 0, beltLen/2 - 1].forEach(ox => {
          const leg = new THREE.Mesh(new THREE.BoxGeometry(0.25, 0.9, 1.6), matMetal);
          leg.position.set(zone.x + ox, 0.45, zone.z + oz);
          g.add(leg);
        });
        // boxes travelling on belt (static)
        [-beltLen/2 + 4, -2, 2, beltLen/2 - 3].forEach(ox => {
          const cube = new THREE.Mesh(new THREE.BoxGeometry(1.0, 0.9, 1.0), matBody);
          cube.position.set(zone.x + ox, 1.45, zone.z + oz);
          g.add(cube);
        });
      });
      break;
    }

    // -------- Manufacturing --------
    case 'cnc': {
      // Four CNC machine cells in a 2x2 layout behind the device row
      const cells = [[-6, -4], [-6, 2], [2, -4], [2, 2]];
      cells.forEach(([ox, oz]) => {
        const body = new THREE.Mesh(new THREE.BoxGeometry(4.0, 2.6, 3.4), matBody);
        body.position.set(zone.x + ox, 1.3, zone.z + oz);
        g.add(body);
        const top = new THREE.Mesh(new THREE.BoxGeometry(4.2, 0.18, 3.6), matMetal);
        top.position.set(zone.x + ox, 2.69, zone.z + oz);
        g.add(top);
        // Control pillar with a screen
        const pillar = new THREE.Mesh(new THREE.BoxGeometry(0.6, 1.8, 0.6), matBodyDark);
        pillar.position.set(zone.x + ox + 2.5, 0.9, zone.z + oz);
        g.add(pillar);
        const screen = new THREE.Mesh(
          new THREE.BoxGeometry(0.04, 0.7, 0.5),
          new THREE.MeshStandardMaterial({ color:0x7A9CFF, emissive:0x3A6FF8, emissiveIntensity:0.7 })
        );
        screen.position.set(zone.x + ox + 2.82, 1.4, zone.z + oz);
        g.add(screen);
        // Spindle indicator on top
        const ind = new THREE.Mesh(new THREE.SphereGeometry(0.18, 10, 8), matAccent);
        ind.position.set(zone.x + ox - 1.5, 2.95, zone.z + oz - 1.2);
        g.add(ind);
      });
      break;
    }
    case 'assembly': {
      // Long central conveyor with three robotic arm stations + workstation boxes
      const beltLen = zone.w * 0.78;
      const belt = new THREE.Mesh(new THREE.BoxGeometry(beltLen, 0.18, 1.4), matBelt);
      belt.position.set(zone.x, 0.85, zone.z + 1.5);
      g.add(belt);
      [-0.75, 0.75].forEach(or => {
        const rail = new THREE.Mesh(new THREE.BoxGeometry(beltLen, 0.6, 0.12), matMetal);
        rail.position.set(zone.x, 0.55, zone.z + 1.5 + or);
        g.add(rail);
      });
      // legs
      for (let i = -beltLen/2 + 1; i <= beltLen/2 - 1; i += 4) {
        const leg = new THREE.Mesh(new THREE.BoxGeometry(0.25, 0.85, 1.4), matMetal);
        leg.position.set(zone.x + i, 0.42, zone.z + 1.5);
        g.add(leg);
      }
      // robotic arm stations along the back
      [-7, 0, 7].forEach(ox => {
        const base = new THREE.Mesh(new THREE.CylinderGeometry(0.6, 0.7, 0.5, 12), matMetal);
        base.position.set(zone.x + ox, 0.25, zone.z - 2);
        g.add(base);
        // shoulder block
        const shoulder = new THREE.Mesh(new THREE.BoxGeometry(0.7, 1.0, 0.7), matBody);
        shoulder.position.set(zone.x + ox, 0.95, zone.z - 2);
        g.add(shoulder);
        // upper arm (tilted)
        const arm = new THREE.Mesh(new THREE.BoxGeometry(0.4, 2.0, 0.4), matBody);
        arm.position.set(zone.x + ox + 0.4, 2.0, zone.z - 1.4);
        arm.rotation.x = -Math.PI / 4;
        g.add(arm);
        // tool head
        const tool = new THREE.Mesh(new THREE.SphereGeometry(0.22, 10, 8), matAccent);
        tool.position.set(zone.x + ox + 0.9, 2.6, zone.z - 0.4);
        g.add(tool);
        // small parts bin near the station
        const bin = new THREE.Mesh(new THREE.BoxGeometry(1.2, 0.4, 0.9), matPallet);
        bin.position.set(zone.x + ox - 1.8, 0.2, zone.z - 3.4);
        g.add(bin);
      });
      // parts on belt
      [-beltLen/2 + 4, -2, 2, beltLen/2 - 3].forEach(ox => {
        const part = new THREE.Mesh(new THREE.BoxGeometry(0.8, 0.5, 0.8), matBody);
        part.position.set(zone.x + ox, 1.18, zone.z + 1.5);
        g.add(part);
      });
      break;
    }
    case 'paint': {
      // Enclosed booth on the right side of the zone, open toward the device row
      const bx = zone.x + 1, bz = zone.z - 1.5;
      const bw = 12, bh = 3.6, bd = 7;
      // back + side walls (3 sides only — front open)
      const wMat = new THREE.MeshStandardMaterial({ color:0x2E3658, roughness:0.9 });
      const back = new THREE.Mesh(new THREE.BoxGeometry(bw, bh, 0.25), wMat);
      back.position.set(bx, bh/2, bz - bd/2);
      const left = new THREE.Mesh(new THREE.BoxGeometry(0.25, bh, bd), wMat);
      left.position.set(bx - bw/2, bh/2, bz);
      const right = new THREE.Mesh(new THREE.BoxGeometry(0.25, bh, bd), wMat);
      right.position.set(bx + bw/2, bh/2, bz);
      // roof of booth
      const roofMesh = new THREE.Mesh(new THREE.BoxGeometry(bw + 0.3, 0.2, bd + 0.3), matBodyDark);
      roofMesh.position.set(bx, bh + 0.1, bz);
      g.add(back, left, right, roofMesh);
      // paint heads (two robotic sprayers on the ceiling)
      [-2, 2].forEach(ox => {
        const arm = new THREE.Mesh(new THREE.BoxGeometry(0.25, 1.6, 0.25), matMetal);
        arm.position.set(bx + ox, bh - 0.8, bz);
        g.add(arm);
        const nozzle = new THREE.Mesh(new THREE.ConeGeometry(0.18, 0.45, 8), matAccent);
        nozzle.position.set(bx + ox, bh - 1.8, bz);
        nozzle.rotation.x = Math.PI;
        g.add(nozzle);
      });
      // Car body silhouette being painted (a stretched box for theatre)
      const carBody = new THREE.Mesh(new THREE.BoxGeometry(4.4, 1.2, 1.8), matBody);
      carBody.position.set(bx, 0.8, bz);
      g.add(carBody);
      // Exhaust duct going up out of the roof
      const duct = new THREE.Mesh(new THREE.CylinderGeometry(0.5, 0.5, 1.4, 12), matMetal);
      duct.position.set(bx - 3.8, bh + 0.9, bz - 1.6);
      g.add(duct);
      // Warning beacon at booth entrance
      const beacon = new THREE.Mesh(
        new THREE.SphereGeometry(0.25, 12, 8),
        new THREE.MeshStandardMaterial({ color:0xE0A13B, emissive:0xE0A13B, emissiveIntensity:0.7 })
      );
      beacon.position.set(bx + bw/2 - 0.3, bh - 0.3, bz + bd/2 - 0.3);
      g.add(beacon);
      break;
    }
    case 'plant': {
      // Compressor with horizontal receiver tank + AHU box + ductwork rack
      // Air receiver tank (horizontal cylinder)
      const tank = new THREE.Mesh(new THREE.CylinderGeometry(0.9, 0.9, 5.0, 16), matBody);
      tank.rotation.z = Math.PI / 2;
      tank.position.set(zone.x - 6, 1.0, zone.z - 4);
      g.add(tank);
      // tank end caps
      [-2.6, 2.6].forEach(ox => {
        const cap = new THREE.Mesh(new THREE.CylinderGeometry(0.92, 0.92, 0.12, 16), matMetal);
        cap.rotation.z = Math.PI / 2;
        cap.position.set(zone.x - 6 + ox, 1.0, zone.z - 4);
        g.add(cap);
      });
      // Compressor head
      const head = new THREE.Mesh(new THREE.BoxGeometry(2.4, 2.0, 1.4), matBodyDark);
      head.position.set(zone.x - 2.8, 1.0, zone.z - 4);
      g.add(head);
      // AHU box
      const ahu = new THREE.Mesh(new THREE.BoxGeometry(7.0, 2.6, 3.2), matBody);
      ahu.position.set(zone.x + 3, 1.3, zone.z - 3.5);
      g.add(ahu);
      // AHU vents (slats facing forward)
      for (let i = 0; i < 5; i++) {
        const v = new THREE.Mesh(new THREE.BoxGeometry(0.9, 0.16, 0.06), matBodyDark);
        v.position.set(zone.x + 0.5 + i * 1.2, 1.2 + (i%2)*0.4, zone.z - 1.91);
        g.add(v);
      }
      // overhead ductwork running across zone
      const ductMat = new THREE.MeshStandardMaterial({ color:PAL.prop.metal, roughness:0.6, metalness:0.4 });
      const ductMain = new THREE.Mesh(new THREE.BoxGeometry(zone.w * 0.85, 0.7, 0.9), ductMat);
      ductMain.position.set(zone.x, 3.6, zone.z + 4);
      g.add(ductMain);
      [-7, -2, 3, 7].forEach(ox => {
        const drop = new THREE.Mesh(new THREE.BoxGeometry(0.4, 0.7, 0.6), ductMat);
        drop.position.set(zone.x + ox, 3.0, zone.z + 4);
        g.add(drop);
      });
      break;
    }

    // -------- HVAC / hotel --------
    case 'vrf-outdoor': {
      // Three stacked Samsung-style VRF condenser units along the back, refrigerant pipe rack
      const yBase = 0.4;
      [-7, 0, 7].forEach(ox => {
        // Lower body (two-fan unit)
        const cab = new THREE.Mesh(new THREE.BoxGeometry(4.0, 2.4, 1.8), matBody);
        cab.position.set(zone.x + ox, yBase + 1.2, zone.z - 4);
        g.add(cab);
        // Top plate
        const top = new THREE.Mesh(new THREE.BoxGeometry(4.2, 0.15, 2.0), matBodyDark);
        top.position.set(zone.x + ox, yBase + 2.48, zone.z - 4);
        g.add(top);
        // Two fan rings on top
        [-1.0, 1.0].forEach(fx => {
          const fanHole = new THREE.Mesh(
            new THREE.CylinderGeometry(0.7, 0.7, 0.12, 16),
            new THREE.MeshStandardMaterial({ color:0x0E1426, roughness:0.95 })
          );
          fanHole.position.set(zone.x + ox + fx, yBase + 2.55, zone.z - 4);
          g.add(fanHole);
          // fan blade silhouette
          for (let b = 0; b < 3; b++) {
            const blade = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.04, 1.0), matMetal);
            blade.position.set(zone.x + ox + fx, yBase + 2.61, zone.z - 4);
            blade.rotation.y = (b * Math.PI * 2) / 3;
            g.add(blade);
          }
        });
        // Front grille slats
        for (let s = 0; s < 6; s++) {
          const slat = new THREE.Mesh(new THREE.BoxGeometry(3.6, 0.08, 0.05), matBodyDark);
          slat.position.set(zone.x + ox, yBase + 0.4 + s * 0.32, zone.z - 3.11);
          g.add(slat);
        }
        // Concrete plinth
        const plinth = new THREE.Mesh(new THREE.BoxGeometry(4.4, 0.4, 2.4), matBodyDark);
        plinth.position.set(zone.x + ox, 0.2, zone.z - 4);
        g.add(plinth);
      });
      // Refrigerant pipe rack running across the back
      const pipeMat = new THREE.MeshStandardMaterial({ color:PAL.prop.metal, roughness:0.45, metalness:0.55 });
      [-0.15, 0.15].forEach(oy => {
        const pipe = new THREE.Mesh(new THREE.CylinderGeometry(0.12, 0.12, zone.w * 0.88, 12), pipeMat);
        pipe.rotation.z = Math.PI / 2;
        pipe.position.set(zone.x, 3.6 + oy, zone.z - 2);
        g.add(pipe);
      });
      // Pipe stands
      [-9, -3, 3, 9].forEach(ox => {
        const stand = new THREE.Mesh(new THREE.BoxGeometry(0.18, 1.4, 0.18), pipeMat);
        stand.position.set(zone.x + ox, 2.9, zone.z - 2);
        g.add(stand);
      });
      break;
    }
    case 'chiller': {
      // Two chiller bodies + buffer tank + pumps
      [-5, 5].forEach(ox => {
        const body = new THREE.Mesh(new THREE.BoxGeometry(7.0, 2.6, 2.6), matBody);
        body.position.set(zone.x + ox, 1.3, zone.z - 2);
        g.add(body);
        // top heat-exchanger fins (vertical slats)
        for (let i = 0; i < 9; i++) {
          const fin = new THREE.Mesh(new THREE.BoxGeometry(0.04, 0.7, 2.4), matMetal);
          fin.position.set(zone.x + ox - 3.0 + i * 0.75, 2.95, zone.z - 2);
          g.add(fin);
        }
        // status display on side
        const disp = new THREE.Mesh(
          new THREE.BoxGeometry(0.04, 0.5, 0.8),
          new THREE.MeshStandardMaterial({ color:0x7A9CFF, emissive:0x3A6FF8, emissiveIntensity:0.7 })
        );
        disp.position.set(zone.x + ox - 3.52, 1.8, zone.z - 2);
        g.add(disp);
      });
      // Buffer tank (tall cylinder)
      const tank = new THREE.Mesh(new THREE.CylinderGeometry(1.0, 1.0, 3.0, 16), matBody);
      tank.position.set(zone.x, 1.5, zone.z + 2.5);
      g.add(tank);
      const tankTop = new THREE.Mesh(new THREE.CylinderGeometry(1.05, 1.05, 0.12, 16), matMetal);
      tankTop.position.set(zone.x, 3.06, zone.z + 2.5);
      g.add(tankTop);
      // Two pumps
      [-3, 3].forEach(ox => {
        const pumpBase = new THREE.Mesh(new THREE.BoxGeometry(1.4, 0.4, 1.0), matBodyDark);
        pumpBase.position.set(zone.x + ox, 0.2, zone.z + 3);
        const pumpBody = new THREE.Mesh(new THREE.CylinderGeometry(0.5, 0.5, 1.0, 12), matMetal);
        pumpBody.rotation.z = Math.PI / 2;
        pumpBody.position.set(zone.x + ox, 0.8, zone.z + 3);
        g.add(pumpBase, pumpBody);
      });
      break;
    }
    case 'guest-rooms': {
      // Show a strip of 5 hotel guest rooms with internal partitions, beds, and ceiling AC units
      const rooms = 5;
      const roomW = (zone.w - 2) / rooms;
      const roomD = 8;
      const startX = zone.x - zone.w/2 + 1 + roomW/2;
      const rowZ = zone.z - 4;
      // Corridor floor strip
      const corridor = new THREE.Mesh(new THREE.PlaneGeometry(zone.w - 2, 4), new THREE.MeshStandardMaterial({ color:0x1F2640, roughness:0.95 }));
      corridor.rotation.x = -Math.PI/2;
      corridor.position.set(zone.x, 0.06, zone.z + 2);
      g.add(corridor);
      // Partitions between rooms
      const partMat = new THREE.MeshStandardMaterial({ color:0x2E3658, roughness:0.9 });
      for (let i = 0; i <= rooms; i++) {
        const px = zone.x - zone.w/2 + 1 + i * roomW;
        const partWall = new THREE.Mesh(new THREE.BoxGeometry(0.15, 2.6, roomD), partMat);
        partWall.position.set(px, 1.3, rowZ);
        g.add(partWall);
      }
      // Back wall of guest rooms
      const backWall = new THREE.Mesh(new THREE.BoxGeometry(zone.w - 2, 2.6, 0.15), partMat);
      backWall.position.set(zone.x, 1.3, rowZ - roomD/2);
      g.add(backWall);
      // Per-room: bed + ceiling AC unit
      for (let i = 0; i < rooms; i++) {
        const rx = startX + i * roomW;
        // bed
        const bed = new THREE.Mesh(new THREE.BoxGeometry(roomW * 0.7, 0.4, 3.0), new THREE.MeshStandardMaterial({ color:0x3A435E, roughness:1 }));
        bed.position.set(rx, 0.25, rowZ - 2.2);
        g.add(bed);
        // pillow
        const pillow = new THREE.Mesh(new THREE.BoxGeometry(roomW * 0.6, 0.12, 0.5), new THREE.MeshStandardMaterial({ color:0xE6E8EE, roughness:1 }));
        pillow.position.set(rx, 0.52, rowZ - 3.55);
        g.add(pillow);
        // nightstand
        const ns = new THREE.Mesh(new THREE.BoxGeometry(0.6, 0.5, 0.6), matBodyDark);
        ns.position.set(rx + roomW * 0.4, 0.3, rowZ - 3.5);
        g.add(ns);
        // Ceiling-mounted Samsung WindFree 4-way unit (small flat box)
        const acBox = new THREE.Mesh(
          new THREE.BoxGeometry(1.8, 0.25, 1.8),
          new THREE.MeshStandardMaterial({ color:0xF6F7F9, roughness:0.6 })
        );
        acBox.position.set(rx, 2.4, rowZ - 0.5);
        g.add(acBox);
        // Vents on the AC unit
        const acVent = new THREE.Mesh(
          new THREE.BoxGeometry(1.5, 0.05, 0.04),
          new THREE.MeshStandardMaterial({ color:0x7A9CFF, emissive:0x3A6FF8, emissiveIntensity:0.5 })
        );
        acVent.position.set(rx, 2.3, rowZ - 0.5);
        g.add(acVent);
      }
      break;
    }
    case 'ahu-room': {
      // Two large AHU boxes + ductwork manifold
      [-6, 6].forEach(ox => {
        const ahu = new THREE.Mesh(new THREE.BoxGeometry(8.0, 2.6, 3.0), matBody);
        ahu.position.set(zone.x + ox, 1.3, zone.z - 4);
        g.add(ahu);
        // Front face dot-grid (filter pattern)
        for (let r = 0; r < 3; r++) {
          for (let c = 0; c < 8; c++) {
            const dot = new THREE.Mesh(new THREE.SphereGeometry(0.08, 8, 6), matBodyDark);
            dot.position.set(zone.x + ox - 3.2 + c * 0.9, 0.5 + r * 0.9, zone.z - 2.51);
            g.add(dot);
          }
        }
        // Label panel (emissive strip)
        const label = new THREE.Mesh(
          new THREE.BoxGeometry(2.0, 0.3, 0.05),
          new THREE.MeshStandardMaterial({ color:0x7A9CFF, emissive:0x3A6FF8, emissiveIntensity:0.6 })
        );
        label.position.set(zone.x + ox, 2.4, zone.z - 2.46);
        g.add(label);
      });
      // Ductwork manifold above
      const ductMat = new THREE.MeshStandardMaterial({ color:PAL.prop.metal, roughness:0.5, metalness:0.5 });
      const mainDuct = new THREE.Mesh(new THREE.BoxGeometry(zone.w * 0.85, 0.9, 1.2), ductMat);
      mainDuct.position.set(zone.x, 3.6, zone.z + 0.5);
      g.add(mainDuct);
      [-9, -4, 1, 6, 9].forEach(ox => {
        const drop = new THREE.Mesh(new THREE.BoxGeometry(0.5, 1.0, 0.7), ductMat);
        drop.position.set(zone.x + ox, 3.0, zone.z + 0.5);
        g.add(drop);
      });
      // Decorative seating cluster (atrium)
      const seatMat = new THREE.MeshStandardMaterial({ color:0x3A435E, roughness:1 });
      [[-3, 4], [3, 4]].forEach(([ox, oz]) => {
        const seat = new THREE.Mesh(new THREE.BoxGeometry(2.2, 0.5, 0.9), seatMat);
        seat.position.set(zone.x + ox, 0.3, zone.z + oz);
        g.add(seat);
        const back = new THREE.Mesh(new THREE.BoxGeometry(2.2, 0.7, 0.2), seatMat);
        back.position.set(zone.x + ox, 0.8, zone.z + oz + 0.4);
        g.add(back);
      });
      break;
    }

    // -------- Campus --------
    case 'lecture': {
      // Tiered seating: 5 rows stepping back and up
      const benchMat = new THREE.MeshStandardMaterial({ color:0x3A435E, roughness:1 });
      const tierMat  = new THREE.MeshStandardMaterial({ color:0x232A48, roughness:1 });
      for (let r = 0; r < 5; r++) {
        const y = 0.15 + r * 0.25;
        const z = zone.z - 4 + r * 1.6;
        // tier platform
        const tier = new THREE.Mesh(new THREE.BoxGeometry(zone.w * 0.8, 0.3, 1.5), tierMat);
        tier.position.set(zone.x, y, z);
        g.add(tier);
        // bench (seat) along this tier
        const bench = new THREE.Mesh(new THREE.BoxGeometry(zone.w * 0.75, 0.4, 0.6), benchMat);
        bench.position.set(zone.x, y + 0.35, z - 0.2);
        g.add(bench);
        // backrest for previous tier's bench (visual cue)
        if (r > 0) {
          const back = new THREE.Mesh(new THREE.BoxGeometry(zone.w * 0.75, 0.5, 0.1), benchMat);
          back.position.set(zone.x, y + 0.55, z - 0.55);
          g.add(back);
        }
      }
      // Lectern at front
      const lectern = new THREE.Mesh(new THREE.BoxGeometry(1.4, 1.2, 0.8), matBody);
      lectern.position.set(zone.x - 4, 0.6, zone.z + 7);
      g.add(lectern);
      // Top tilted panel
      const top = new THREE.Mesh(new THREE.BoxGeometry(1.4, 0.08, 0.9), matMetal);
      top.position.set(zone.x - 4, 1.22, zone.z + 7);
      top.rotation.x = -Math.PI / 16;
      g.add(top);
      // Projector screen at back wall
      const screen = new THREE.Mesh(
        new THREE.BoxGeometry(zone.w * 0.55, 2.2, 0.08),
        new THREE.MeshStandardMaterial({ color:0xF6F7F9, roughness:0.7 })
      );
      screen.position.set(zone.x, 2.4, zone.z + 8.5);
      g.add(screen);
      // Screen content (faint accent rectangle)
      const slide = new THREE.Mesh(
        new THREE.BoxGeometry(zone.w * 0.55 - 0.3, 2.0, 0.02),
        new THREE.MeshStandardMaterial({ color:0x3A6FF8, emissive:0x3A6FF8, emissiveIntensity:0.25 })
      );
      slide.position.set(zone.x, 2.4, zone.z + 8.46);
      g.add(slide);
      break;
    }
    case 'classroom': {
      // 4 columns × 3 rows of desks (12 desks) with chairs and a teacher's desk + whiteboard
      const deskMat = new THREE.MeshStandardMaterial({ color:0xF1ECDD, roughness:1 });
      const chairMat = new THREE.MeshStandardMaterial({ color:0x3A435E, roughness:1 });
      const colsX = [-7.5, -2.5, 2.5, 7.5];
      const rowsZ = [-2, 1, 4];
      colsX.forEach(ox => {
        rowsZ.forEach(oz => {
          const desk = new THREE.Mesh(new THREE.BoxGeometry(2.4, 0.08, 1.2), deskMat);
          desk.position.set(zone.x + ox, 0.75, zone.z + oz);
          g.add(desk);
          [-0.8, 0.8].forEach(lx => {
            const leg = new THREE.Mesh(new THREE.BoxGeometry(0.08, 0.7, 0.08), matMetal);
            leg.position.set(zone.x + ox + lx, 0.35, zone.z + oz);
            g.add(leg);
          });
          const chair = new THREE.Mesh(new THREE.BoxGeometry(1.0, 0.5, 1.0), chairMat);
          chair.position.set(zone.x + ox, 0.25, zone.z + oz + 1.1);
          g.add(chair);
          const back = new THREE.Mesh(new THREE.BoxGeometry(1.0, 0.7, 0.1), chairMat);
          back.position.set(zone.x + ox, 0.6, zone.z + oz + 1.55);
          g.add(back);
        });
      });
      // Teacher's desk
      const tDesk = new THREE.Mesh(new THREE.BoxGeometry(3.4, 0.1, 1.2), matBody);
      tDesk.position.set(zone.x, 0.8, zone.z - 6);
      g.add(tDesk);
      // Whiteboard
      const board = new THREE.Mesh(
        new THREE.BoxGeometry(zone.w * 0.55, 1.8, 0.08),
        new THREE.MeshStandardMaterial({ color:0xFFFFFF, roughness:0.9 })
      );
      board.position.set(zone.x, 2.1, zone.z - 9.5);
      g.add(board);
      // Whiteboard accent stripe
      const stripe = new THREE.Mesh(
        new THREE.BoxGeometry(zone.w * 0.55 + 0.05, 0.05, 0.085),
        new THREE.MeshStandardMaterial({ color:0x3A6FF8 })
      );
      stripe.position.set(zone.x, 1.05, zone.z - 9.5);
      g.add(stripe);
      break;
    }
    case 'study': {
      // Six privacy booths (cylinder shells) arranged in 2 rows + central long table
      const podMat = new THREE.MeshStandardMaterial({ color:0x2E3658, roughness:0.95 });
      const tablemat = new THREE.MeshStandardMaterial({ color:0xF1ECDD, roughness:1 });
      const positions = [
        [-7, -3], [-7, 3], [-2, -3], [-2, 3], [3, -3], [3, 3],
      ];
      positions.forEach(([ox, oz]) => {
        // Outer pod shell — partial cylinder (we fake with a 16-sided cylinder)
        const shell = new THREE.Mesh(
          new THREE.CylinderGeometry(1.4, 1.4, 1.8, 16, 1, true),
          new THREE.MeshStandardMaterial({ color:0x2E3658, roughness:0.95, side:THREE.DoubleSide })
        );
        shell.position.set(zone.x + ox, 0.9, zone.z + oz);
        g.add(shell);
        // Floor inside pod (a small disc)
        const padding = new THREE.Mesh(
          new THREE.CylinderGeometry(1.35, 1.35, 0.05, 16),
          new THREE.MeshStandardMaterial({ color:0x232A48, roughness:1 })
        );
        padding.position.set(zone.x + ox, 0.04, zone.z + oz);
        g.add(padding);
        // Desktop inside pod
        const desktop = new THREE.Mesh(new THREE.BoxGeometry(1.6, 0.06, 0.6), tablemat);
        desktop.position.set(zone.x + ox, 0.7, zone.z + oz - 0.4);
        g.add(desktop);
        // Stool
        const stool = new THREE.Mesh(new THREE.CylinderGeometry(0.3, 0.3, 0.45, 12), podMat);
        stool.position.set(zone.x + ox, 0.22, zone.z + oz + 0.3);
        g.add(stool);
        // Reading lamp (small emissive sphere on top)
        const lamp = new THREE.Mesh(
          new THREE.SphereGeometry(0.14, 10, 8),
          new THREE.MeshStandardMaterial({ color:0xE0A13B, emissive:0xE0A13B, emissiveIntensity:0.5 })
        );
        lamp.position.set(zone.x + ox + 0.6, 1.4, zone.z + oz - 0.5);
        g.add(lamp);
      });
      // Central long table (collaborative area)
      const longTable = new THREE.Mesh(new THREE.BoxGeometry(zone.w * 0.6, 0.1, 1.2), tablemat);
      longTable.position.set(zone.x, 0.75, zone.z + 7);
      g.add(longTable);
      // Bench seats either side
      [-0.9, 0.9].forEach(oz => {
        const bench = new THREE.Mesh(new THREE.BoxGeometry(zone.w * 0.55, 0.5, 0.55), podMat);
        bench.position.set(zone.x, 0.25, zone.z + 7 + oz);
        g.add(bench);
      });
      break;
    }
    case 'office': {
      // 2 rows of 3 desks each, with monitors + chairs, plus a round meeting table
      const deskMat = new THREE.MeshStandardMaterial({ color:0xF1ECDD, roughness:1 });
      const monitorMat = new THREE.MeshStandardMaterial({ color:0x0A0F1F, roughness:0.8 });
      const monitorScreenMat = new THREE.MeshStandardMaterial({ color:0x7A9CFF, emissive:0x3A6FF8, emissiveIntensity:0.4 });
      const chairMat = new THREE.MeshStandardMaterial({ color:0x232A48, roughness:1 });
      const desksX = [-7, -2, 3];
      const rowsZ = [-3, 2];
      desksX.forEach(ox => {
        rowsZ.forEach((oz, ri) => {
          const desk = new THREE.Mesh(new THREE.BoxGeometry(3.0, 0.08, 1.4), deskMat);
          desk.position.set(zone.x + ox, 0.78, zone.z + oz);
          g.add(desk);
          // Legs
          [[-1.3,-0.55],[1.3,-0.55],[-1.3,0.55],[1.3,0.55]].forEach(([lx,lz]) => {
            const leg = new THREE.Mesh(new THREE.BoxGeometry(0.08, 0.74, 0.08), matMetal);
            leg.position.set(zone.x + ox + lx, 0.41, zone.z + oz + lz);
            g.add(leg);
          });
          // Monitor stand + screen
          const stand = new THREE.Mesh(new THREE.BoxGeometry(0.18, 0.32, 0.18), matMetal);
          stand.position.set(zone.x + ox, 0.98, zone.z + oz - 0.3);
          g.add(stand);
          const screen = new THREE.Mesh(new THREE.BoxGeometry(1.4, 0.85, 0.06), monitorMat);
          screen.position.set(zone.x + ox, 1.45, zone.z + oz - 0.3);
          g.add(screen);
          const screenFace = new THREE.Mesh(new THREE.BoxGeometry(1.3, 0.75, 0.005), monitorScreenMat);
          screenFace.position.set(zone.x + ox, 1.45, zone.z + oz - 0.27);
          g.add(screenFace);
          // Office chair
          const chairSeat = new THREE.Mesh(new THREE.CylinderGeometry(0.5, 0.4, 0.12, 12), chairMat);
          chairSeat.position.set(zone.x + ox, 0.55, zone.z + oz + (ri===0 ? 1.4 : 1.4));
          g.add(chairSeat);
          const chairBack = new THREE.Mesh(new THREE.BoxGeometry(0.9, 0.9, 0.12), chairMat);
          chairBack.position.set(zone.x + ox, 1.05, zone.z + oz + (ri===0 ? 1.85 : 1.85));
          g.add(chairBack);
          const chairLeg = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.05, 0.4, 8), matMetal);
          chairLeg.position.set(zone.x + ox, 0.3, zone.z + oz + 1.4);
          g.add(chairLeg);
        });
      });
      // Round meeting table
      const mtg = new THREE.Mesh(new THREE.CylinderGeometry(1.4, 1.4, 0.08, 24), deskMat);
      mtg.position.set(zone.x + 8, 0.78, zone.z + 5);
      g.add(mtg);
      const mtgLeg = new THREE.Mesh(new THREE.CylinderGeometry(0.18, 0.3, 0.74, 12), matMetal);
      mtgLeg.position.set(zone.x + 8, 0.37, zone.z + 5);
      g.add(mtgLeg);
      // Meeting chairs around it
      for (let a = 0; a < 4; a++) {
        const ang = (a / 4) * Math.PI * 2;
        const cx = zone.x + 8 + Math.cos(ang) * 2.2;
        const cz = zone.z + 5 + Math.sin(ang) * 2.2;
        const c = new THREE.Mesh(new THREE.BoxGeometry(0.7, 0.8, 0.7), chairMat);
        c.position.set(cx, 0.4, cz);
        g.add(c);
      }
      break;
    }
  }
  return g;
}

// =============================================================
// Component
// =============================================================
const Twin3D = ({ devices, site, selectedId, onSelect }) => {
  const containerRef = React.useRef(null);
  const overlayRef   = React.useRef(null);
  const stateRef     = React.useRef({});
  const [hovered, setHovered] = React.useState(null);
  const [roof, setRoof] = React.useState(false);
  const selectedRef = React.useRef(selectedId);
  React.useEffect(() => { selectedRef.current = selectedId; }, [selectedId]);

  // Init scene once
  React.useEffect(() => {
    const container = containerRef.current;
    if (!container || !window.THREE) return;
    const THREE = window.THREE;
    const W = container.clientWidth || 900;
    const H = container.clientHeight || 600;

    const scene = new THREE.Scene();
    scene.background = new THREE.Color(0x0A0F1F);
    scene.fog = new THREE.Fog(0x0A0F1F, 90, 240);

    const camera = new THREE.PerspectiveCamera(36, W/H, 0.1, 500);
    camera.position.set(58, 48, 78);

    const renderer = new THREE.WebGLRenderer({ antialias:true, alpha:false });
    renderer.setSize(W, H);
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    container.appendChild(renderer.domElement);

    // Lighting — soft, slightly directional, low-poly friendly
    scene.add(new THREE.AmbientLight(0xB4C0DA, 0.55));
    const key = new THREE.DirectionalLight(0xFFFFFF, 0.5);
    key.position.set(50, 90, 30);
    scene.add(key);
    const fill = new THREE.DirectionalLight(0x7A9CFF, 0.3);
    fill.position.set(-40, 50, -30);
    scene.add(fill);
    const rim = new THREE.DirectionalLight(0x3A6FF8, 0.18);
    rim.position.set(0, 20, -80);
    scene.add(rim);

    // Ground (outside the building)
    const ground = new THREE.Mesh(
      new THREE.PlaneGeometry(260, 260),
      new THREE.MeshStandardMaterial({ color:PAL.ground, roughness:1, metalness:0 })
    );
    ground.rotation.x = -Math.PI/2;
    ground.position.y = -0.02;
    scene.add(ground);

    // Subtle grid hint
    const grid = new THREE.GridHelper(260, 52, 0x162038, 0x0E1224);
    grid.material.transparent = true;
    grid.material.opacity = 0.5;
    scene.add(grid);

    // Orbit controls
    const controls = new THREE.OrbitControls(camera, renderer.domElement);
    controls.target.set(0, 1, 0);
    controls.enableDamping = true;
    controls.dampingFactor = 0.08;
    controls.maxPolarAngle = Math.PI / 2.05;
    controls.minDistance = 30;
    controls.maxDistance = 180;
    controls.enablePan = true;

    // Raycaster
    const raycaster = new THREE.Raycaster();
    const pointer = new THREE.Vector2();

    const onPointerMove = (e) => {
      const rect = renderer.domElement.getBoundingClientRect();
      pointer.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
      pointer.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
      raycaster.setFromCamera(pointer, camera);
      const meshes = (stateRef.current.deviceMeshes || []).map(d => d.mesh);
      const hit = raycaster.intersectObjects(meshes, false)[0];
      const id = hit ? hit.object.userData.deviceId : null;
      if (id !== stateRef.current.hoveredId) {
        stateRef.current.hoveredId = id;
        setHovered(id);
        renderer.domElement.style.cursor = id ? 'pointer' : 'grab';
      }
    };
    const onClick = (e) => {
      const rect = renderer.domElement.getBoundingClientRect();
      pointer.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
      pointer.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
      raycaster.setFromCamera(pointer, camera);
      const meshes = (stateRef.current.deviceMeshes || []).map(d => d.mesh);
      const hit = raycaster.intersectObjects(meshes, false)[0];
      if (hit && stateRef.current.onSelect) {
        stateRef.current.onSelect(hit.object.userData.device);
      }
    };
    renderer.domElement.addEventListener('pointermove', onPointerMove);
    renderer.domElement.addEventListener('click', onClick);

    // Resize
    const onResize = () => {
      const w = container.clientWidth, h = container.clientHeight;
      camera.aspect = w / h;
      camera.updateProjectionMatrix();
      renderer.setSize(w, h);
    };
    window.addEventListener('resize', onResize);
    const ro = new ResizeObserver(onResize);
    ro.observe(container);

    // Render loop
    const clock = new THREE.Clock();
    let raf;
    const tick = () => {
      const t = clock.getElapsedTime();
      (stateRef.current.deviceMeshes || []).forEach(({ mesh, state, ring }) => {
        if (state === 'down' || state === 'idle') {
          mesh.material.emissiveIntensity = 0.25;
        } else {
          const k = 0.5 + 0.5 * Math.sin(t * 2.6);
          mesh.material.emissiveIntensity = 0.45 + k * 0.45;
        }
        if (ring && mesh.userData.deviceId === selectedRef.current) {
          ring.visible = true;
          ring.rotation.z = t * 0.6;
        } else if (ring) {
          ring.visible = false;
        }
      });
      controls.update();
      renderer.render(scene, camera);
      updateOverlay();
      raf = requestAnimationFrame(tick);
    };

    const updateOverlay = () => {
      const overlay = overlayRef.current;
      if (!overlay) return;
      const ws = stateRef.current;
      (ws.roomLabels || []).forEach(({ el, position }) => {
        const v = position.clone().project(camera);
        const x = (v.x * 0.5 + 0.5) * renderer.domElement.clientWidth;
        const y = (-v.y * 0.5 + 0.5) * renderer.domElement.clientHeight;
        if (v.z > 1) { el.style.display = 'none'; }
        else {
          el.style.display = 'block';
          el.style.transform = `translate(-50%, -50%) translate(${x.toFixed(1)}px, ${y.toFixed(1)}px)`;
        }
      });
      if (ws.selectedLabel) {
        const dm = (ws.deviceMeshes || []).find(d => d.mesh.userData.deviceId === selectedRef.current);
        if (dm) {
          const pos = dm.mesh.position.clone();
          pos.y += 1.6;
          const v = pos.project(camera);
          const x = (v.x * 0.5 + 0.5) * renderer.domElement.clientWidth;
          const y = (-v.y * 0.5 + 0.5) * renderer.domElement.clientHeight;
          ws.selectedLabel.style.display = (v.z > 1) ? 'none' : 'block';
          ws.selectedLabel.style.transform = `translate(-50%, -100%) translate(${x.toFixed(1)}px, ${y.toFixed(1)}px)`;
          ws.selectedLabel.textContent = dm.device.id;
        } else {
          ws.selectedLabel.style.display = 'none';
        }
      }
    };

    stateRef.current = { THREE, scene, camera, renderer, controls, deviceMeshes:[], roomLabels:[], onSelect:null };
    raf = requestAnimationFrame(tick);

    return () => {
      cancelAnimationFrame(raf);
      window.removeEventListener('resize', onResize);
      ro.disconnect();
      renderer.domElement.removeEventListener('pointermove', onPointerMove);
      renderer.domElement.removeEventListener('click', onClick);
      controls.dispose();
      renderer.dispose();
      if (renderer.domElement.parentNode === container) container.removeChild(renderer.domElement);
    };
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  React.useEffect(() => { stateRef.current.onSelect = onSelect; }, [onSelect]);

  // Rebuild rooms + devices when site or devices change, or roof toggles
  React.useEffect(() => {
    const ws = stateRef.current;
    if (!ws.scene) return;
    const THREE = ws.THREE;

    ['roomsGroup','devicesGroup','meshGroup','roofGroup'].forEach(k => {
      if (ws[k]) { ws.scene.remove(ws[k]); disposeGroup(ws[k]); ws[k] = null; }
    });
    if (overlayRef.current) {
      ws.roomLabels?.forEach(({ el }) => el.remove());
    }
    ws.roomLabels = [];
    ws.deviceMeshes = [];

    const zones = ZONE_LAYOUTS_3D[site.id] || [];
    const roomsGroup = new THREE.Group();
    const devicesGroup = new THREE.Group();
    const meshGroup = new THREE.Group();
    const roofGroup = new THREE.Group();

    // Materials
    const matExterior = new THREE.MeshStandardMaterial({ color:PAL.exterior, roughness:0.85 });
    const matInterior = new THREE.MeshStandardMaterial({ color:PAL.interior, roughness:0.85 });

    // Building bounds (outer perimeter wrapping all zones)
    if (zones.length) {
      const minX = Math.min(...zones.map(z => z.x - z.w/2)) - 0.5;
      const maxX = Math.max(...zones.map(z => z.x + z.w/2)) + 0.5;
      const minZ = Math.min(...zones.map(z => z.z - z.d/2)) - 0.5;
      const maxZ = Math.max(...zones.map(z => z.z + z.d/2)) + 0.5;
      const totW = maxX - minX, totD = maxZ - minZ;
      const cx = (minX + maxX) / 2, cz = (minZ + maxZ) / 2;
      const wallH = 4.2, wallT = 0.4;

      // Building floor base (just under all zones, so seams are hidden)
      const base = new THREE.Mesh(
        new THREE.BoxGeometry(totW + 0.8, 0.3, totD + 0.8),
        new THREE.MeshStandardMaterial({ color:0x0E1426, roughness:1 })
      );
      base.position.set(cx, -0.15, cz);
      roomsGroup.add(base);

      // Exterior walls — 4 sides
      // N wall
      roomsGroup.add(makeWallSegments(THREE, 'N', cx, minZ, totW, wallH, wallT, false, matExterior));
      // S wall
      roomsGroup.add(makeWallSegments(THREE, 'S', cx, maxZ, totW, wallH, wallT, false, matExterior));
      // W wall
      roomsGroup.add(makeWallSegments(THREE, 'W', minX, cz, totD, wallH, wallT, false, matExterior));
      // E wall
      roomsGroup.add(makeWallSegments(THREE, 'E', maxX, cz, totD, wallH, wallT, false, matExterior));

      // Optional roof (translucent, toggled)
      if (roof) {
        const roofMesh = new THREE.Mesh(
          new THREE.BoxGeometry(totW + 0.4, 0.2, totD + 0.4),
          new THREE.MeshStandardMaterial({ color:PAL.exterior, transparent:true, opacity:0.6, roughness:0.9 })
        );
        roofMesh.position.set(cx, wallH + 0.15, cz);
        roofGroup.add(roofMesh);
      }
    }

    // Per-zone: floor, interior partitions, label
    zones.forEach((z, zi) => {
      // Tinted floor
      const floorColor = PAL.floor[z.kind] || PAL.floor.default;
      const floor = new THREE.Mesh(
        new THREE.PlaneGeometry(z.w, z.d),
        new THREE.MeshStandardMaterial({ color:floorColor, roughness:0.95 })
      );
      floor.rotation.x = -Math.PI/2;
      floor.position.set(z.x, 0.05, z.z);
      roomsGroup.add(floor);

      // Floor tile lines — subtle grid within zone (line segments at 4-unit spacing)
      const gridGeom = new THREE.BufferGeometry();
      const verts = [];
      for (let gx = -z.w/2 + 4; gx < z.w/2; gx += 4) {
        verts.push(z.x + gx, 0.055, z.z - z.d/2);
        verts.push(z.x + gx, 0.055, z.z + z.d/2);
      }
      for (let gz = -z.d/2 + 4; gz < z.d/2; gz += 4) {
        verts.push(z.x - z.w/2, 0.055, z.z + gz);
        verts.push(z.x + z.w/2, 0.055, z.z + gz);
      }
      gridGeom.setAttribute('position', new THREE.Float32BufferAttribute(verts, 3));
      const tileGrid = new THREE.LineSegments(
        gridGeom,
        new THREE.LineBasicMaterial({ color:0x2A325A, transparent:true, opacity:0.35 })
      );
      roomsGroup.add(tileGrid);

      // Interior partition walls — only between zones (not on the building exterior).
      // A partition exists between zone i and zone i+1 along the shared edge.
      const wallH = 4.2, wallT = 0.3;
      // E side wall: only if there's another zone to the east (zi+1)
      const east = zones[zi + 1];
      if (east) {
        // partition runs along Z (vertical wall) at zone's east edge
        const partX = z.x + z.w / 2;
        const partZ = z.z;
        const open = z.doors?.includes('E') || east.doors?.includes('W');
        roomsGroup.add(makeWallSegments(THREE, 'E', partX, partZ, z.d, wallH, wallT, open, matInterior));
      }

      // Interior props
      roomsGroup.add(makeZoneProps(THREE, z));

      // Room label (HTML overlay)
      if (overlayRef.current) {
        const el = document.createElement('div');
        el.innerHTML = `<span style="opacity:.5; font-family:'IBM Plex Mono', monospace; margin-right:8px">${String(zi+1).padStart(2,'0')}</span>${z.name.toUpperCase()}`;
        el.style.cssText = `
          position:absolute; top:0; left:0; pointer-events:none;
          font: 500 10px/1.2 'IBM Plex Sans', sans-serif;
          letter-spacing:0.16em; color:rgba(255,255,255,.75);
          background:rgba(10,15,31,.7); border:1px solid rgba(58,111,248,.4);
          padding:5px 10px; white-space:nowrap;
        `;
        overlayRef.current.appendChild(el);
        ws.roomLabels.push({ el, position: new THREE.Vector3(z.x, 4.6, z.z) });
      }
    });

    // -------- Devices --------
    const siteDevices = devices.filter(d => d.site === site.id);
    const byZone = {};
    siteDevices.forEach(d => { (byZone[d.zone] ||= []).push(d); });

    Object.entries(byZone).forEach(([zoneName, list]) => {
      const z = zones.find(zz => zz.name === zoneName);
      if (!z) return;
      // Layout: place devices in a row along the front of the zone, in front of the props,
      // so markers don't disappear inside equipment.
      const positions = [];
      const usableW = z.w - 6;
      list.forEach((d, i) => {
        const t = list.length === 1 ? 0.5 : i / (list.length - 1);
        const x = z.x - usableW/2 + t * usableW;
        const zPos = z.z + z.d/2 - 3; // toward front of room
        positions.push({ d, x, z: zPos });
      });

      const gateway = positions.find(p => p.d.kind === 'gateway');

      positions.forEach(({ d, x, z: zPos }) => {
        const colorHex = STATE_HEX_3D[d.state] || STATE_HEX_3D.ok;
        // Stem
        const stem = new THREE.Mesh(
          new THREE.CylinderGeometry(0.05, 0.05, 2.4, 8),
          new THREE.MeshStandardMaterial({ color:0x5A5E6E, roughness:0.7 })
        );
        stem.position.set(x, 1.2, zPos);
        devicesGroup.add(stem);

        // Floor disc
        const base = new THREE.Mesh(
          new THREE.CylinderGeometry(0.55, 0.55, 0.05, 24),
          new THREE.MeshStandardMaterial({ color: colorHex, emissive: colorHex, emissiveIntensity:0.4, transparent:true, opacity:0.6 })
        );
        base.position.set(x, 0.09, zPos);
        devicesGroup.add(base);

        // Head sphere
        const mat = new THREE.MeshStandardMaterial({
          color: colorHex, emissive: colorHex,
          emissiveIntensity: 0.55, roughness:0.35, metalness:0.2,
        });
        const head = new THREE.Mesh(new THREE.SphereGeometry(0.5, 24, 16), mat);
        head.position.set(x, 2.55, zPos);
        head.userData = { device:d, deviceId:d.id };
        devicesGroup.add(head);

        // Selection ring
        const ring = new THREE.Mesh(
          new THREE.RingGeometry(0.85, 1.0, 32),
          new THREE.MeshBasicMaterial({ color:0x7A9CFF, side:THREE.DoubleSide, transparent:true, opacity:0.9 })
        );
        ring.position.set(x, 2.55, zPos);
        ring.rotation.x = Math.PI/2;
        ring.visible = false;
        devicesGroup.add(ring);

        ws.deviceMeshes.push({ mesh: head, ring, device: d, state: d.state });

        // Mesh line for sensors → gateway
        if (gateway && d.kind === 'sensor') {
          const points = [
            new THREE.Vector3(x, 2.55, zPos),
            new THREE.Vector3(gateway.x, 2.55, gateway.z),
          ];
          const line = new THREE.Line(
            new THREE.BufferGeometry().setFromPoints(points),
            new THREE.LineBasicMaterial({ color:0x3A6FF8, transparent:true, opacity:0.4 })
          );
          meshGroup.add(line);
        }
      });
    });

    ws.scene.add(roomsGroup);
    ws.scene.add(meshGroup);
    ws.scene.add(devicesGroup);
    ws.scene.add(roofGroup);
    ws.roomsGroup = roomsGroup;
    ws.meshGroup = meshGroup;
    ws.devicesGroup = devicesGroup;
    ws.roofGroup = roofGroup;

    // Camera target = site centre
    if (zones.length) {
      const cx = zones.reduce((s, z) => s + z.x, 0) / zones.length;
      const cz = zones.reduce((s, z) => s + z.z, 0) / zones.length;
      ws.controls.target.set(cx, 1.5, cz);
      ws.controls.update();
    }
  }, [devices, site, roof]);

  // Selected-label HTML element
  React.useEffect(() => {
    if (!overlayRef.current) return;
    const el = document.createElement('div');
    el.style.cssText = `
      position:absolute; top:0; left:0; pointer-events:none;
      font: 500 11px/1 'IBM Plex Mono', monospace;
      color:#FFF; background:#0A0F1F;
      border:1px solid #3A6FF8; padding:5px 10px;
      box-shadow: 0 0 0 3px rgba(58,111,248,.18);
      display:none;
    `;
    overlayRef.current.appendChild(el);
    stateRef.current.selectedLabel = el;
    return () => { el.remove(); };
  }, []);

  const hoveredDevice = hovered ? devices.find(d => d.id === hovered) : null;

  return (
    <div ref={containerRef} className="teo-twin-canvas" style={{
      position:'relative', width:'100%', height:600, background:'#0A0F1F',
      borderTop:'1px solid #EDEDED', borderBottom:'1px solid #EDEDED',
    }}>
      <div ref={overlayRef} style={{ position:'absolute', inset:0, pointerEvents:'none' }} />

      {/* Top-left: navigation hint */}
      <div style={{
        position:'absolute', top:16, left:16, display:'flex', alignItems:'center', gap:12,
        background:'rgba(10,15,31,.7)', border:'1px solid rgba(255,255,255,.12)',
        padding:'8px 12px', font:"400 12px 'IBM Plex Sans'", color:'rgba(255,255,255,.85)'
      }}>
        <i data-lucide="move-3d" style={{ width:14, height:14, color:'#7A9CFF' }} />
        Drag · rotate · scroll · pan
      </div>

      {/* Top-centre: roof toggle */}
      <div style={{
        position:'absolute', top:16, left:'50%', transform:'translateX(-50%)',
        display:'flex', alignItems:'center', gap:8,
        background:'rgba(10,15,31,.7)', border:'1px solid rgba(255,255,255,.12)',
        padding:'6px 8px',
      }}>
        {['Cutaway','Roof on'].map((opt, i) => {
          const active = (i === 0 && !roof) || (i === 1 && roof);
          return (
            <button key={opt} onClick={() => setRoof(i === 1)} style={{
              padding:'5px 10px', font:"400 12px 'IBM Plex Sans'",
              background: active ? '#7A9CFF' : 'transparent',
              color: active ? '#0A0F1F' : 'rgba(255,255,255,.85)',
              border:'1px solid '+(active ? '#7A9CFF' : 'rgba(255,255,255,.18)'),
              cursor:'pointer', pointerEvents:'auto'
            }}>{opt}</button>
          );
        })}
      </div>

      {/* Top-right: legend */}
      <div style={{
        position:'absolute', top:16, right:16, display:'flex', alignItems:'center', gap:14,
        background:'rgba(10,15,31,.7)', border:'1px solid rgba(255,255,255,.12)',
        padding:'8px 12px', font:"400 11px 'IBM Plex Sans'", color:'rgba(255,255,255,.85)'
      }}>
        {[['ok','Online','#3A6FF8'],['warn','Degraded','#E0A13B'],['down','Offline','#C44A4A'],['idle','Idle','#878787']].map(([s,l,c]) => (
          <div key={s} style={{ display:'flex', alignItems:'center', gap:6 }}>
            <span style={{ width:7, height:7, borderRadius:'50%', background:c, boxShadow:`0 0 6px ${c}` }} />
            {l}
          </div>
        ))}
      </div>

      {/* Bottom-left: hover card */}
      {hoveredDevice && (
        <div style={{
          position:'absolute', bottom:16, left:16,
          background:'#0A0F1F', color:'#FFF',
          border:'1px solid rgba(255,255,255,.18)', padding:'12px 16px',
          font:"400 12px 'IBM Plex Sans'", maxWidth:280
        }}>
          <div style={{ fontFamily:'IBM Plex Mono, monospace', fontSize:11, color:'#7A9CFF' }}>{hoveredDevice.id}</div>
          <div style={{ fontSize:14, marginTop:4 }}>{hoveredDevice.name}</div>
          <div style={{ display:'flex', alignItems:'center', gap:10, marginTop:8, color:'rgba(255,255,255,.7)' }}>
            <span style={{ display:'inline-flex', alignItems:'center', gap:6 }}>
              <span style={{ width:6, height:6, borderRadius:'50%', background:`#${STATE_HEX_3D[hoveredDevice.state].toString(16).padStart(6,'0')}` }} />
              {hoveredDevice.state}
            </span>
            <span>·</span>
            <span style={{ fontFamily:'IBM Plex Mono, monospace' }}>{hoveredDevice.metric.value} {hoveredDevice.metric.unit}</span>
          </div>
        </div>
      )}
    </div>
  );
};

function disposeGroup(group) {
  group.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();
    }
  });
}

Object.assign(window, { Twin3D });
