/* SkyOps — Zone report digital twin (Three.js)
   The zone rebuilt in 3D from the report: tilted tables on posts, every module an instance
   coloured by its report state, CoA 3 beacons, the drone flight path that produced the data. */

const { useEffect: useEffectZT, useRef: useRefZT, useState: useStateZT } = React;

const ZT_PW = 1.13, ZT_PH = 2.28, ZT_TILT = 25 * Math.PI / 180, ZT_ROW = 9.5, ZT_TGAP = 2.2;

function ztPos(Z, p) {
  const tableW = Z.TCOLS * (ZT_PW + 0.02);
  const x = p.table * (tableW + ZT_TGAP) + p.tc * (ZT_PW + 0.02) + ZT_PW / 2;
  const along = (p.tr - 0.5) * (ZT_PH + 0.02);           // along the tilted plane
  const z = p.row * ZT_ROW + along * Math.cos(ZT_TILT);
  const y = 1.6 - along * Math.sin(ZT_TILT);
  return { x, y, z };
}
function ztExtent(Z) {
  const tableW = Z.TCOLS * (ZT_PW + 0.02);
  return { w: Z.TABLES * tableW + (Z.TABLES - 1) * ZT_TGAP, d: (Z.ROWS - 1) * ZT_ROW };
}
function ztColorOf(p, mode, Z) {
  if (mode === "thermal") return window.zrIron((p.tmax - (Z.T_BASE - 3)) / 34);
  if (mode === "soiling") return `rgb(${Math.round(27 + p.soil * 150)},${Math.round(47 + p.soil * 90)},${Math.round(79 - p.soil * 20)})`;
  if (p.type === "hot_cell") return { 1: "#eab308", 2: "#f97316", 3: "#ef4444" }[p.cls];
  if (p.type === "bypass") return "#f97316";
  if (p.type === "cracked") return "#eab308";
  if (p.type === "pid") return "#8b5cf6";
  if (p.type === "offline") return "#d946ef";
  if (p.soil > 0.6) return "#a8844a";
  if (p.soil > 0.35) return "#6e6440";
  return "#1b2f4f";
}

function ZoneTwin3D({ sel, setSel, mode, filter }) {
  const Z = window.ZR;
  const mountRef = useRefZT(null);
  const S = useRefZT({});
  const [hover, setHover] = useStateZT(null);
  const [opts, setOpts] = useStateZT({ path: true, beacons: true });
  const [view, setView] = useStateZT("oblique");

  // Build scene once per zone
  useEffectZT(() => {
    const THREE = window.THREE, mount = mountRef.current;
    if (!THREE || !mount) return;
    const W = mount.clientWidth, H = mount.clientHeight;
    const ext = ztExtent(Z), cx = ext.w / 2, cz = ext.d / 2;

    const scene = new THREE.Scene();
    scene.background = new THREE.Color(0x0a0f18);
    scene.fog = new THREE.Fog(0x0a0f18, 160, 420);
    const camera = new THREE.PerspectiveCamera(40, W / H, 0.5, 2000);
    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setPixelRatio(Math.min(2, window.devicePixelRatio));
    renderer.setSize(W, H);
    mount.innerHTML = ""; mount.appendChild(renderer.domElement);

    scene.add(new THREE.HemisphereLight(0xbcd4ff, 0x1a2412, 0.9));
    const sun = new THREE.DirectionalLight(0xfff2dd, 1.1); sun.position.set(cx + 60, 120, cz - 80); scene.add(sun);

    // Ground + gravel road (SW edge) + fence line
    const ground = new THREE.Mesh(new THREE.PlaneGeometry(ext.w + 140, ext.d + 140), new THREE.MeshLambertMaterial({ color: 0x1c2618 }));
    ground.rotation.x = -Math.PI / 2; ground.position.set(cx, 0, cz); scene.add(ground);
    const road = new THREE.Mesh(new THREE.PlaneGeometry(ext.w + 60, 5), new THREE.MeshLambertMaterial({ color: 0x5c5444 }));
    road.rotation.x = -Math.PI / 2; road.position.set(cx - 10, 0.02, ext.d + 10); scene.add(road);
    const fenceGeo = new THREE.EdgesGeometry(new THREE.BoxGeometry(ext.w + 24, 0.01, ext.d + 24));
    const fence = new THREE.LineSegments(fenceGeo, new THREE.LineBasicMaterial({ color: 0x3b4a5c })); fence.position.set(cx, 0.05, cz); scene.add(fence);

    // Table structures (torque tube + posts)
    const tubeMat = new THREE.MeshLambertMaterial({ color: 0x6b7280 });
    const tableW = Z.TCOLS * (ZT_PW + 0.02);
    for (let r = 0; r < Z.ROWS; r++) for (let t = 0; t < Z.TABLES; t++) {
      const x0 = t * (tableW + ZT_TGAP), z0 = r * ZT_ROW;
      const tube = new THREE.Mesh(new THREE.BoxGeometry(tableW, 0.12, 0.12), tubeMat);
      tube.position.set(x0 + tableW / 2, 1.5, z0); scene.add(tube);
      for (let k = 0; k <= 3; k++) {
        const post = new THREE.Mesh(new THREE.BoxGeometry(0.12, 1.5, 0.12), tubeMat);
        post.position.set(x0 + 0.3 + k * (tableW - 0.6) / 3, 0.75, z0); scene.add(post);
      }
    }

    // Modules — one instanced mesh, instance id === panel index
    const geo = new THREE.BoxGeometry(ZT_PW, 0.04, ZT_PH);
    const mat = new THREE.MeshStandardMaterial({ roughness: 0.35, metalness: 0.15 });
    const mesh = new THREE.InstancedMesh(geo, mat, Z.N);
    const m4 = new THREE.Matrix4(), q = new THREE.Quaternion(), e = new THREE.Euler(ZT_TILT, 0, 0), one = new THREE.Vector3(1, 1, 1);
    q.setFromEuler(e);
    Z.panels.forEach(p => { const P = ztPos(Z, p); m4.compose(new THREE.Vector3(P.x, P.y, P.z), q, one); mesh.setMatrixAt(p.i, m4); mesh.setColorAt(p.i, new THREE.Color(0x1b2f4f)); });
    scene.add(mesh);

    // Selection outline
    const selBox = new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(ZT_PW + 0.25, 0.3, ZT_PH + 0.25)), new THREE.LineBasicMaterial({ color: 0xffffff }));
    selBox.quaternion.copy(q); scene.add(selBox);

    // CoA 3 beacons
    const beacons = new THREE.Group();
    const bMat = new THREE.MeshBasicMaterial({ color: 0xef4444, transparent: true, opacity: 0.85 });
    Z.panels.filter(p => p.cls === 3).forEach(p => {
      const P = ztPos(Z, p);
      const stem = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.05, 6, 6), bMat); stem.position.set(P.x, P.y + 3.2, P.z);
      const head = new THREE.Mesh(new THREE.SphereGeometry(0.45, 12, 10), bMat); head.position.set(P.x, P.y + 6.4, P.z);
      beacons.add(stem, head);
    });
    scene.add(beacons);

    // Drone flight path (lawnmower along rows at 38 m AGL) + capture points
    const pathG = new THREE.Group();
    const pts = [], AGL = 38;
    for (let r = 0; r < Z.ROWS; r++) {
      const z = r * ZT_ROW, a = -8, b = ext.w + 8;
      if (r % 2 === 0) { pts.push(new THREE.Vector3(a, AGL, z), new THREE.Vector3(b, AGL, z)); }
      else { pts.push(new THREE.Vector3(b, AGL, z), new THREE.Vector3(a, AGL, z)); }
    }
    const curve = new THREE.CurvePath(); for (let k = 0; k < pts.length - 1; k++) curve.add(new THREE.LineCurve3(pts[k], pts[k + 1]));
    pathG.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints(pts), new THREE.LineBasicMaterial({ color: 0x4a90d9, transparent: true, opacity: 0.8 })));
    const capMat = new THREE.MeshBasicMaterial({ color: 0x7fb6ff });
    for (let k = 0; k < pts.length - 1; k += 2) {
      for (let s = 0; s <= 1.0001; s += 1 / 8) {
        const v = pts[k].clone().lerp(pts[k + 1], s);
        const d = new THREE.Mesh(new THREE.SphereGeometry(0.25, 6, 5), capMat); d.position.copy(v); pathG.add(d);
      }
    }
    const drone = new THREE.Group();
    const body = new THREE.Mesh(new THREE.BoxGeometry(1.4, 0.35, 1.4), new THREE.MeshBasicMaterial({ color: 0xe2e8f0 }));
    drone.add(body);
    [[-1, -1], [1, -1], [-1, 1], [1, 1]].forEach(([a, b]) => { const r = new THREE.Mesh(new THREE.CylinderGeometry(0.55, 0.55, 0.05, 14), new THREE.MeshBasicMaterial({ color: 0x4a90d9, transparent: true, opacity: 0.7 })); r.position.set(a * 0.9, 0.2, b * 0.9); drone.add(r); });
    const cone = new THREE.Mesh(new THREE.ConeGeometry(9, AGL - 2, 4, 1, true), new THREE.MeshBasicMaterial({ color: 0x4a90d9, transparent: true, opacity: 0.07, side: THREE.DoubleSide }));
    cone.position.y = -(AGL - 2) / 2; cone.rotation.y = Math.PI / 4; drone.add(cone);
    pathG.add(drone);
    scene.add(pathG);

    // Orbit controls (manual)
    const cam = { yaw: -0.65, pitch: 0.62, dist: 150, tx: cx, tz: cz, ty: 0 };
    const goal = { ...cam };
    const apply = () => {
      const r = cam.dist * Math.cos(cam.pitch);
      camera.position.set(cam.tx + r * Math.sin(cam.yaw), cam.ty + cam.dist * Math.sin(cam.pitch), cam.tz + r * Math.cos(cam.yaw));
      camera.lookAt(cam.tx, cam.ty, cam.tz);
    };
    let drag = null;
    const el = renderer.domElement;
    const onDown = (ev) => { drag = { x: ev.clientX, y: ev.clientY, moved: false, pan: ev.button === 2 || ev.shiftKey }; };
    const onMove = (ev) => {
      if (drag) {
        const dx = ev.clientX - drag.x, dy = ev.clientY - drag.y;
        if (Math.abs(dx) + Math.abs(dy) > 3) drag.moved = true;
        if (drag.pan) { const s = cam.dist / 600; goal.tx -= (dx * Math.cos(cam.yaw) + dy * Math.sin(cam.yaw)) * s * 2; goal.tz -= (-dx * Math.sin(cam.yaw) + dy * Math.cos(cam.yaw)) * s * 2; }
        else { goal.yaw -= dx * 0.006; goal.pitch = Math.max(0.12, Math.min(1.5, goal.pitch + dy * 0.005)); }
        drag.x = ev.clientX; drag.y = ev.clientY;
      } else S.current.hoverAt = { x: ev.clientX, y: ev.clientY };
    };
    const ray = new THREE.Raycaster(), ndc = new THREE.Vector2();
    const pickAt = (x, y) => {
      const rc = el.getBoundingClientRect();
      ndc.set(((x - rc.left) / rc.width) * 2 - 1, -((y - rc.top) / rc.height) * 2 + 1);
      ray.setFromCamera(ndc, camera);
      const hit = ray.intersectObject(mesh, false)[0];
      return hit ? hit.instanceId : null;
    };
    const onUp = (ev) => { if (drag && !drag.moved) { const id = pickAt(ev.clientX, ev.clientY); if (id != null) S.current.onPick(id); } drag = null; };
    const onWheel = (ev) => { ev.preventDefault(); goal.dist = Math.max(18, Math.min(420, goal.dist * (1 + ev.deltaY * 0.001))); };
    const onCtx = (ev) => ev.preventDefault();
    const onLeave = () => { S.current.hoverAt = null; setHover(null); };
    el.addEventListener("mousedown", onDown); window.addEventListener("mousemove", onMove); window.addEventListener("mouseup", onUp);
    el.addEventListener("wheel", onWheel, { passive: false }); el.addEventListener("contextmenu", onCtx); el.addEventListener("mouseleave", onLeave);
    const ro = new ResizeObserver(() => { const w = mount.clientWidth, h = mount.clientHeight; renderer.setSize(w, h); camera.aspect = w / h; camera.updateProjectionMatrix(); });
    ro.observe(mount);

    let running = true, t0 = performance.now(), lastHover = 0;
    const len = curve.getLength();
    const frame = () => {
      if (!running) return;
      const now = performance.now();
      ["yaw", "pitch", "dist", "tx", "tz", "ty"].forEach(k => cam[k] += (goal[k] - cam[k]) * 0.12);
      apply();
      const u = ((now - t0) / 1000 / 46) % 1;
      const pnt = curve.getPointAt(u), pnt2 = curve.getPointAt(Math.min(0.999, u + 0.001));
      drone.position.copy(pnt); drone.lookAt(pnt2.x, pnt.y, pnt2.z);
      bMat.opacity = 0.55 + 0.4 * Math.abs(Math.sin(now / 420));
      if (S.current.hoverAt && now - lastHover > 80) {
        lastHover = now; const id = pickAt(S.current.hoverAt.x, S.current.hoverAt.y);
        if (id !== S.current.lastHoverId) { S.current.lastHoverId = id; setHover(id); }
      }
      renderer.render(scene, camera);
    };
    const loop = () => { frame(); if (running) S.current.raf = requestAnimationFrame(loop); };
    loop();
    const fb = setInterval(() => { if (document.hidden || !S.current.raf) frame(); }, 120);

    Object.assign(S.current, { THREE, mesh, selBox, beacons, pathG, goal, cam, ext, len });
    S.current.apply = null;
    return () => {
      running = false; cancelAnimationFrame(S.current.raf); clearInterval(fb); ro.disconnect();
      el.removeEventListener("mousedown", onDown); window.removeEventListener("mousemove", onMove); window.removeEventListener("mouseup", onUp);
      el.removeEventListener("wheel", onWheel); el.removeEventListener("contextmenu", onCtx); el.removeEventListener("mouseleave", onLeave);
      renderer.dispose(); mount.innerHTML = "";
    };
  }, [Z]);

  S.current.onPick = setSel;

  // Colours
  useEffectZT(() => {
    const { THREE, mesh } = S.current; if (!mesh) return;
    const c = new THREE.Color();
    Z.panels.forEach(p => {
      c.set(ztColorOf(p, mode, Z));
      const dim = filter === "thermal" ? p.type === "ok" : filter === "cleaning" ? p.soil <= 0.35 : false;
      if (dim) c.multiplyScalar(0.22);
      mesh.setColorAt(p.i, c);
    });
    mesh.instanceColor.needsUpdate = true;
  }, [mode, filter, Z]);

  // Selection
  useEffectZT(() => {
    const { selBox } = S.current; if (!selBox) return;
    const P = ztPos(Z, Z.panels[sel]); selBox.position.set(P.x, P.y, P.z);
  }, [sel, Z]);

  useEffectZT(() => { if (S.current.beacons) S.current.beacons.visible = opts.beacons; if (S.current.pathG) S.current.pathG.visible = opts.path; }, [opts]);

  const preset = (v) => {
    setView(v);
    const g = S.current.goal, ext = S.current.ext; if (!g) return;
    const P = ztPos(Z, Z.panels[sel]);
    if (v === "oblique") Object.assign(g, { yaw: -0.65, pitch: 0.62, dist: 150, tx: ext.w / 2, tz: ext.d / 2 });
    if (v === "top") Object.assign(g, { yaw: 0, pitch: 1.5, dist: 170, tx: ext.w / 2, tz: ext.d / 2 });
    if (v === "drone") Object.assign(g, { yaw: Math.PI * 0.92, pitch: 0.34, dist: 80, tx: ext.w / 2, tz: ext.d * 0.2 });
    if (v === "focus") Object.assign(g, { yaw: g.yaw, pitch: 0.55, dist: 22, tx: P.x, tz: P.z });
  };

  const hp = hover != null ? Z.panels[hover] : null;

  return (
    <div className="zt">
      <div ref={mountRef} className="zt-canvas"/>
      <div className="zt-tools">
        {[["oblique", "Oblique"], ["top", "Top-down"], ["drone", "Drone POV"], ["focus", "Fly to module"]].map(([k, l]) => (
          <button key={k} className={view === k ? "on" : ""} onClick={() => preset(k)}>{l}</button>
        ))}
      </div>
      <div className="zt-opts">
        <label><input type="checkbox" checked={opts.path} onChange={e => setOpts(o => ({ ...o, path: e.target.checked }))}/>Flight path · 38 m AGL</label>
        <label><input type="checkbox" checked={opts.beacons} onChange={e => setOpts(o => ({ ...o, beacons: e.target.checked }))}/>CoA 3 beacons</label>
      </div>
      <div className="zt-hint mono">drag rotate · shift/right-drag pan · wheel zoom · click module</div>
      {hp && (
        <div className="zt-hover">
          <b className="mono">{hp.id}</b>
          <span>{hp.type === "ok" ? (hp.soil > 0.35 ? `Soiling ${Math.round(hp.soil * 100)}%` : "Healthy") : `${hp.type.replace("_", " ")} · ΔT +${hp.dT} K${hp.cls ? ` · CoA ${hp.cls}` : ""}`}</span>
          <span className="mono muted">Tmax {hp.tmax.toFixed(1)} °C</span>
        </div>
      )}
      <div className="zt-src">
        <span className="caps">Twin sources</span>
        <span>Geometry · DSM + mesh from this flight</span>
        <span>Layout · module registration {window.ZR.N.toLocaleString()}/{window.ZR.N.toLocaleString()}</span>
        <span>State · report {window.ZR.zoneId}</span>
      </div>
    </div>
  );
}

window.ZoneTwin3D = ZoneTwin3D;
