import { Suspense, useRef, useMemo, useEffect, type ReactNode, useState } from 'react';
import { Canvas, useThree, useFrame } from '@react-three/fiber';
import { OrbitControls, useGLTF, Grid, Html, Line, PerspectiveCamera, OrthographicCamera } from '@react-three/drei';
import { Loader2 } from 'lucide-react';
import * as THREE from 'three';
import { useModelStore, type SceneModel } from '@/stores/useModelStore';
import { findNearestVertex, detectHoleAtFace, findNearestEdgeSegment, detectCircularEdgeAtPoint } from './SmartMeasure';
import { registerModelLocalGroup, unregisterModelLocalGroup, getModelWorldScaleFactor } from '@/lib/modelTransforms';
import { parseIFCtoThree } from '@/lib/convertIFC';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { mainCameraRef, mainControlsRef, viewAnim, calibration, pushModelFaceNormal, computeCalibrationQuaternion, subscribeCalibration } from './viewCubeBus';

interface ModelViewerProps {
  url?: string; // legacy, ignored — uses store.models
}

function isPickableModelMesh(obj: THREE.Object3D): obj is THREE.Mesh {
  if (!(obj instanceof THREE.Mesh)) return false;
  if (obj.userData.__edgeLine) return false;
  if (obj.geometry instanceof THREE.SphereGeometry) return false;
  if (obj.geometry instanceof THREE.RingGeometry) return false;
  if (obj.geometry instanceof THREE.PlaneGeometry) return false;
  return true;
}

/** Walk up the parent chain until we find a node tagged as `ifcElement`.
 *  Falls back to the clicked mesh so older/demo GLBs without IFC metadata
 *  still support selection/highlight. */
export function findElementRoot(obj: THREE.Object3D | null): THREE.Object3D | null {
  const fallback = isPickableModelMesh(obj as THREE.Object3D) ? obj : null;
  let cur: THREE.Object3D | null = obj;
  while (cur) {
    if (cur.userData?.ifcElement) return cur;
    cur = cur.parent;
  }
  return fallback;
}

function hasIfcAncestor(obj: THREE.Object3D): boolean {
  let cur = obj.parent;
  while (cur) {
    if (cur.userData?.ifcElement) return true;
    cur = cur.parent;
  }
  return false;
}

/** Stable key for an element across reloads: prefers ifcId, falls back to name. */
export function elementKey(modelId: string, el: THREE.Object3D): string {
  const id = el.userData?.ifcId ?? el.name ?? el.uuid;
  return `${modelId}:${id}`;
}

/** Globally accessible ref to the active R3F scene (set by SceneRefCapture). */
export const sceneRef: { current: THREE.Scene | null } = { current: null };

function SceneRefCapture() {
  const { scene, camera } = useThree();
  useEffect(() => {
    sceneRef.current = scene;
    return () => { if (sceneRef.current === scene) sceneRef.current = null; };
  }, [scene]);
  useEffect(() => {
    mainCameraRef.current = camera;
    return () => {
      if (mainCameraRef.current === camera) mainCameraRef.current = null;
    };
  }, [camera]);
  return null;
}

/** Drives the camera animation requested by the ViewCube. */
function ViewCubeAnimator() {
  const { camera } = useThree();
  useFrame((_state, delta) => {
    if (!viewAnim.active) return;
    viewAnim.t = Math.min(1, viewAnim.t + delta / viewAnim.duration);
    const k = viewAnim.t < 0.5
      ? 2 * viewAnim.t * viewAnim.t
      : 1 - Math.pow(-2 * viewAnim.t + 2, 2) / 2;
    camera.position.lerpVectors(viewAnim.startPos, viewAnim.endPos, k);
    camera.up.lerpVectors(viewAnim.startUp, viewAnim.endUp, k).normalize();
    const controls = mainControlsRef.current;
    if (controls?.target) camera.lookAt(controls.target);
    controls?.update?.();
    if (viewAnim.t >= 1) viewAnim.active = false;
  });
  return null;
}

/** Switches between perspective and orthographic projection while keeping
 *  the visual framing (position, target, on-screen size of the scene). */
function CameraSwitcher() {
  const mode = useModelStore((s) => s.cameraMode);
  const { size } = useThree();
  // Snapshot current view from whichever camera is active right now, before swap.
  const snap = useMemo(() => {
    const c = mainCameraRef.current;
    const ctrl = mainControlsRef.current;
    const target = (ctrl?.target ?? new THREE.Vector3()).clone();
    const pos = c ? c.position.clone() : new THREE.Vector3(2, 2, 2);
    const up = c ? c.up.clone() : new THREE.Vector3(0, 1, 0);
    const dist = pos.distanceTo(target);
    // Compute ortho zoom that matches current perspective framing.
    let zoom = 100;
    if (c && (c as THREE.PerspectiveCamera).isPerspectiveCamera) {
      const fov = ((c as THREE.PerspectiveCamera).fov ?? 50) * (Math.PI / 180);
      const worldHeight = 2 * Math.max(dist, 0.001) * Math.tan(fov / 2);
      zoom = Math.max(1, size.height / Math.max(worldHeight, 1e-6));
    } else if (c && (c as THREE.OrthographicCamera).isOrthographicCamera) {
      zoom = (c as THREE.OrthographicCamera).zoom;
    }
    return { target, pos, up, zoom };
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [mode]);
  // Sync OrbitControls target after camera swap.
  useEffect(() => {
    const ctrl = mainControlsRef.current;
    if (ctrl?.target) ctrl.target.copy(snap.target);
    ctrl?.update?.();
  }, [snap]);
  if (mode === 'ortho') {
    return (
      <OrthographicCamera
        makeDefault
        position={snap.pos.toArray()}
        up={snap.up.toArray()}
        zoom={snap.zoom}
        near={-1000}
        far={1000}
      />
    );
  }
  return (
    <PerspectiveCamera
      makeDefault
      position={snap.pos.toArray()}
      up={snap.up.toArray()}
      fov={50}
      near={0.001}
      far={1000}
    />
  );
}

function LoadingFallback() {
  return (
    <Html center>
      <div className="flex items-center gap-2 rounded-lg bg-card px-4 py-2 text-foreground shadow-lg">
        <Loader2 className="h-4 w-4 animate-spin text-primary" />
        <span className="font-mono text-sm">Carregando modelo…</span>
      </div>
    </Html>
  );
}

function GLBModel({ sceneModel, isActive }: { sceneModel: SceneModel; isActive: boolean }) {
  const [rawScene, setRawScene] = useState<THREE.Object3D | null>(null);
  const scene = useMemo(() => rawScene ? rawScene.clone(true) : null, [rawScene]);

  useEffect(() => {
    let active = true;
    const isIfc = sceneModel.fileName.toLowerCase().endsWith('.ifc');
    if (isIfc) {
      fetch(sceneModel.url)
        .then((res) => res.arrayBuffer())
        .then((buf) => parseIFCtoThree(buf))
        .then((threeScene) => {
          if (active) setRawScene(threeScene);
        })
        .catch((err) => console.error('[GLBModel] IFC parsing error', err));
    } else {
      const loader = new GLTFLoader();
      loader.load(
        sceneModel.url,
        (gltf) => {
          if (active) setRawScene(gltf.scene);
        },
        undefined,
        (err) => console.error('[GLBModel] GLTF loading error', err)
      );
    }
    return () => {
      active = false;
    };
  }, [sceneModel.url, sceneModel.fileName]);

  const ref = useRef<THREE.Group>(null);
  const opacity = useModelStore((s) => s.opacity);
  const renderMode = useModelStore((s) => s.renderMode);
  const wireframeColor = useModelStore((s) => s.wireframeColor);
  const wireframeThickness = useModelStore((s) => s.wireframeThickness);
  const edgeThresholdAngle = useModelStore((s) => s.edgeThresholdAngle);
  const checklist = useModelStore((s) => s.checklist);
  const setActive = useModelStore((s) => s.setActiveModel);
  const selectionMode = useModelStore((s) => s.selectionMode);
  const measureMode = useModelStore((s) => s.measureMode);
  const fineTuning = sceneModel.fineTuning;

  // Re-render when calibration state changes (so calQuat resets to identity during the flow).
  const [, setCalTick] = useState(0);
  useEffect(() => subscribeCalibration(() => setCalTick(t => t + 1)), []);

  const modelInfo = useMemo(() => {
    if (!scene) return { size: new THREE.Vector3(), center: new THREE.Vector3() };
    const box = new THREE.Box3().setFromObject(scene);
    const size = new THREE.Vector3();
    const center = new THREE.Vector3();
    box.getSize(size);
    box.getCenter(center);
    return { size, center };
  }, [scene]);

  const originalColors = useRef<Map<THREE.Material, THREE.Color>>(new Map());

  useEffect(() => {
    if (!scene) return;
    const hasRejected = checklist.some(i => i.status === 'rejected');
    const allApproved = checklist.every(i => i.status === 'approved');

    scene.traverse((child) => {
      if (child instanceof THREE.Mesh) {
        if (Array.isArray(child.material)) {
          child.material = child.material.map(m => m.clone());
        } else {
          child.material = child.material.clone();
        }

        const toRemove: THREE.Object3D[] = [];
        child.children.forEach(c => {
          if (c.userData.__edgeLine) toRemove.push(c);
        });
        toRemove.forEach(c => {
          if (c instanceof THREE.LineSegments) {
            c.geometry.dispose();
            (c.material as THREE.Material).dispose();
          }
          child.remove(c);
        });

        const materials = Array.isArray(child.material) ? child.material : [child.material];
        materials.forEach((mat: THREE.Material) => {
          if (mat instanceof THREE.MeshStandardMaterial) {
            if (!originalColors.current.has(mat)) {
              originalColors.current.set(mat, mat.color.clone());
            }

            if (renderMode === 'edges' && !measureMode) {
              mat.visible = false;
            } else {
              mat.visible = true;
              const targetOpacity = measureMode ? 0.25 : opacity;
              mat.transparent = targetOpacity < 1;
              mat.opacity = targetOpacity;
              mat.wireframe = renderMode === 'wireframe';
              if (renderMode === 'wireframe') {
                mat.wireframeLinewidth = wireframeThickness;
                mat.color.set(wireframeColor);
              } else if (hasRejected) {
                mat.color.setHSL(0, 0.7, 0.5);
              } else if (allApproved) {
                mat.color.setHSL(0.38, 0.7, 0.45);
              } else {
                // Tint with the per-model color (subtle)
                mat.color.set(sceneModel.color);
              }
            }
            mat.needsUpdate = true;
          }
        });

        if ((renderMode === 'edges' || measureMode) && child.geometry) {
          const edgesGeo = new THREE.EdgesGeometry(child.geometry, edgeThresholdAngle);
          const lineMat = new THREE.LineBasicMaterial({
            color: measureMode ? '#00f3ff' : wireframeColor,
            linewidth: measureMode ? 2 : wireframeThickness,
            toneMapped: false,
            transparent: true,
            opacity: 0.95,
          });
          const lineSegments = new THREE.LineSegments(edgesGeo, lineMat);
          lineSegments.userData.__edgeLine = true;
          child.add(lineSegments);
        }
      }
    });
  }, [scene, opacity, renderMode, checklist, wireframeColor, wireframeThickness, edgeThresholdAngle, sceneModel.color, measureMode]);

  const rotXRad = (fineTuning.rotX * Math.PI) / 180;
  const rotYRad = (fineTuning.rotY * Math.PI) / 180;
  const rotZRad = (fineTuning.rotZ * Math.PI) / 180;
  const s = fineTuning.scale ?? 1;
  const scaleRatio = useModelStore((st) => st.scaleRatio);
  const renderFactor = scaleRatio?.factor ?? 1;

  // Calibration quaternion (applied innermost, around center). We render with identity
  // while calibration is in progress for this model so face normals reflect the
  // un-calibrated frame; the result is committed at the end.
  const calQuatArr = sceneModel.calibrationQuat;
  const isCalibratingThis = calibration.modelId === sceneModel.id && calibration.step !== 'idle' && calibration.step !== 'done';
  const calQuat = useMemo(() => {
    if (isCalibratingThis) return new THREE.Quaternion();
    if (!calQuatArr) return new THREE.Quaternion();
    return new THREE.Quaternion(calQuatArr[0], calQuatArr[1], calQuatArr[2], calQuatArr[3]);
  }, [calQuatArr, isCalibratingThis]);
  const calGroupRef = useRef<THREE.Group>(null);

  if (!sceneModel.visible) return null;
  if (!scene) return null;

  // Determine dominant local axis of the model (longest bbox side) for axial roll
  const dominantAxis: 'x' | 'y' | 'z' =
    modelInfo.size.x >= modelInfo.size.y && modelInfo.size.x >= modelInfo.size.z
      ? 'x'
      : modelInfo.size.y >= modelInfo.size.z
      ? 'y'
      : 'z';

  return (
    <group
      scale={[renderFactor, renderFactor, renderFactor]}
      onClick={(e) => {
        e.stopPropagation();
        if ((e.delta ?? 0) > 4) return;

        // Calibration: capture model face normal in world space.
        if (
          calibration.modelId === sceneModel.id &&
          (calibration.step === 'await-model-1' || calibration.step === 'await-model-2' || calibration.step === 'await-model-3') &&
          e.face && e.object
        ) {
          const n = e.face.normal.clone();
          const nm = new THREE.Matrix3().getNormalMatrix(e.object.matrixWorld);
          n.applyMatrix3(nm).normalize();
          const wq = new THREE.Quaternion();
          (calGroupRef.current ?? e.object).getWorldQuaternion(wq);
          pushModelFaceNormal(n, wq);
          // Apply / refresh calibration whenever we have ≥2 pairs.
          if (calibration.pairs.length >= 2) {
            const q = computeCalibrationQuaternion(calibration.pairs);
            if (q) {
              useModelStore.getState().setCalibration(sceneModel.id, [q.x, q.y, q.z, q.w]);
            }
          }
          return;
        }

        if (selectionMode) {
          const element = findElementRoot(e.object);
          if (element) {
            useModelStore.getState().toggleElementSelection(elementKey(sceneModel.id, element));
          }
          return;
        }

        setActive(sceneModel.id);
      }}
      userData={{ modelId: sceneModel.id, dominantAxis }}
    >
      {/* Translation */}
      <group position={[fineTuning.posX, fineTuning.posY, fineTuning.posZ]}>
        {/* Rotation + scale around the geometry center */}
        <group
          ref={ref}
          rotation={[rotXRad, rotYRad, rotZRad]}
          scale={[s, s, s]}
        >
          {/* Calibration rotation (innermost, around center). */}
          <group ref={calGroupRef} quaternion={calQuat}>
            <ModelLocalFrame modelId={sceneModel.id} center={modelInfo.center}>
              <primitive object={scene} />
              <ModelMeasurements modelId={sceneModel.id} />
            </ModelLocalFrame>
          </group>
        </group>
      </group>
    </group>
  );
}

/** Innermost local frame of a model. Registers itself in the global model
 *  registry so the store can convert world points to this group's local frame. */
function ModelLocalFrame({
  modelId,
  center,
  children,
}: {
  modelId: string;
  center: THREE.Vector3;
  children: ReactNode;
}) {
  const ref = useRef<THREE.Group>(null);
  useEffect(() => {
    const g = ref.current;
    if (!g) return;
    registerModelLocalGroup(modelId, g);
    return () => unregisterModelLocalGroup(modelId, g);
  }, [modelId]);
  return (
    <group ref={ref} position={[-center.x, -center.y, -center.z]}>
      {children}
    </group>
  );
}

function SceneModels() {
  const models = useModelStore((s) => s.models);
  const activeId = useModelStore((s) => s.activeModelId);
  return (
    <>
      {models.map((m) => (
        <GLBModel key={m.id} sceneModel={m} isActive={m.id === activeId} />
      ))}
    </>
  );
}

/** World-units-per-pixel for the active camera (persp or ortho). */
function computeWorldPerPixel(
  camera: THREE.Camera,
  size: { height: number },
  worldPos: THREE.Vector3,
): number {
  const ortho = camera as THREE.OrthographicCamera;
  if ((ortho as any).isOrthographicCamera) {
    const visibleHeight = (ortho.top - ortho.bottom) / (ortho.zoom || 1);
    return visibleHeight / Math.max(1, size.height);
  }
  const persp = camera as THREE.PerspectiveCamera;
  const dist = camera.position.distanceTo(worldPos);
  const fov = (persp.fov ?? 50) * (Math.PI / 180);
  return (2 * dist * Math.tan(fov / 2)) / Math.max(1, size.height);
}

/** Sphere marker at a 3D point — scaled per-frame to keep constant screen size. */
function PointMarker({ position, color = '#e8a838' }: { position: [number, number, number]; color?: string }) {
  const ref = useRef<THREE.Mesh>(null);
  const { camera, size } = useThree();
  const tmpVec = useMemo(() => new THREE.Vector3(), []);
  const worldPos = useMemo(() => new THREE.Vector3(), []);
  useFrame(() => {
    if (!ref.current) return;
    ref.current.getWorldPosition(worldPos);
    const wpp = computeWorldPerPixel(camera, size, worldPos);
    const targetWorld = Math.max(0.0005, 6 * wpp);
    const parentScale = ref.current.parent ? ref.current.parent.getWorldScale(tmpVec).x || 1 : 1;
    ref.current.scale.setScalar(targetWorld / parentScale);
  });
  return (
    <mesh ref={ref} position={position}>
      <sphereGeometry args={[1, 16, 16]} />
      <meshBasicMaterial color={color} depthTest={false} transparent opacity={0.95} />
    </mesh>
  );
}

/** Snap ring indicator — fixed ~10 mm diameter in world units (follows piece scale).
 *  World units in this app are meters (1 unit = 1000 mm), so 5 mm radius = 0.005
 *  world units, divided by the model's cumulative world scale so it remains
 *  ~10 mm physically when the piece is scaled. */
function SnapRing({ position, color = '#eab308' }: { position: [number, number, number]; color?: string }) {
  const ref = useRef<THREE.Mesh>(null);
  const { camera, size } = useThree();
  const worldPos = useMemo(() => new THREE.Vector3(), []);
  useFrame(() => {
    if (!ref.current) return;
    ref.current.lookAt(camera.position);
    const activeId = useModelStore.getState().activeModelId;
    const sf = getModelWorldScaleFactor(activeId);
    let radiusWorld = 0.005 / Math.max(1e-6, sf); // 5 mm in world units
    if (!Number.isFinite(radiusWorld) || radiusWorld <= 0) {
      ref.current.getWorldPosition(worldPos);
      const wpp = computeWorldPerPixel(camera, size, worldPos);
      radiusWorld = Math.max(0.0008, 8 * wpp);
    }
    ref.current.scale.setScalar(radiusWorld);
  });
  return (
    <mesh ref={ref} position={position}>
      <ringGeometry args={[0.7, 1, 28]} />
      <meshBasicMaterial color={color} side={THREE.DoubleSide} depthTest={false} transparent opacity={0.9} />
    </mesh>
  );
}

function MeasurementLabel({ position, text, variant }: { position: [number, number, number]; text: string; variant: 'success' | 'primary' | 'accent'; fixed?: boolean }) {
  const { camera } = useThree();
  const isOrtho = (camera as THREE.OrthographicCamera).isOrthographicCamera === true;
  // In orthographic mode, drei's distanceFactor uses perspective math and can
  // explode → the HTML label scales massively and visually wipes the model.
  // Use a plain screen-space Html label in ortho; perspective keeps a stable factor.
  const df = isOrtho ? undefined : 1.5;
  const borderClass = variant === 'success'
    ? 'border-success/60'
    : variant === 'accent'
      ? 'border-amber-400/70'
      : 'border-primary/60';
  const textClass = variant === 'success'
    ? 'text-success'
    : variant === 'accent'
      ? 'text-amber-400'
      : 'text-primary';
  return (
    <Html position={position} center distanceFactor={df} zIndexRange={[100, 0]} style={{ pointerEvents: 'none' }}>
      <div className={`rounded bg-card/95 border ${borderClass} px-2 py-0.5 shadow-lg backdrop-blur-sm`}>
        <span className={`font-mono text-[11px] font-bold ${textClass} whitespace-nowrap`}>
          {text}
        </span>
      </div>
    </Html>
  );
}

/** Renders a single measurement's geometry + label.
 *  Coordinates are interpreted in whichever frame the component is mounted in. */
function MeasurementGraphic({ m }: { m: import('@/stores/useModelStore').Measurement }) {
  const a: [number, number, number] = [m.pointA.x, m.pointA.y, m.pointA.z];
  const b: [number, number, number] = [m.pointB.x, m.pointB.y, m.pointB.z];
  const mid: [number, number, number] = [
    (a[0] + b[0]) / 2,
    (a[1] + b[1]) / 2,
    (a[2] + b[2]) / 2,
  ];

  if (m.kind === 'hole') {
    return (
      <group>
        <PointMarker position={a} color="#f59e0b" />
        <MeasurementLabel position={a} text={m.label ?? `Ø ${m.distanceMM.toFixed(1)}`} variant="accent" />
      </group>
    );
  }

  if (m.kind === 'edge') {
    return (
      <group>
        <PointMarker position={a} color="#22c55e" />
        <PointMarker position={b} color="#22c55e" />
        <Line points={[a, b]} color="#22c55e" lineWidth={2} depthTest={false} />
        <MeasurementLabel position={mid} text={m.label ?? `${m.distanceMM.toFixed(1)} mm`} variant="success" />
      </group>
    );
  }

  return (
    <group>
      <PointMarker position={a} color="#22c55e" />
      <PointMarker position={b} color="#22c55e" />
      <Line points={[a, b]} color="#22c55e" lineWidth={2} depthTest={false} />
      <MeasurementLabel position={mid} text={`${m.distanceMM.toFixed(1)} mm`} variant="success" />
    </group>
  );
}

/** Renders measurements attached to a given model, in that model's local frame.
 *  Mounted inside GLBModel so they follow the model when it is repositioned. */
function ModelMeasurements({ modelId }: { modelId: string }) {
  const measurements = useModelStore((s) => s.measurements);
  const attached = measurements.filter((m) => m.modelId === modelId);
  if (attached.length === 0) return null;
  return (
    <>
      {attached.map((m) => (
        <MeasurementGraphic key={m.id} m={m} />
      ))}
    </>
  );
}

/** Renders snap indicator, hover tooltip, pending point and any *legacy*
 *  world-frame measurements that aren't attached to a specific model. */
function MeasurementOverlay() {
  const measurements = useModelStore((s) => s.measurements);
  const measurePoints = useModelStore((s) => s.measurePoints);
  const snapPoint = useModelStore((s) => s.snapPoint);
  const hoverInfo = useModelStore((s) => s.hoverInfo);
  const measureMode = useModelStore((s) => s.measureMode);

  return (
    <>
      {/* Snap indicator */}
      {measureMode && snapPoint && (
        <SnapRing position={[snapPoint.x, snapPoint.y, snapPoint.z]} />
      )}

      {/* Hover auto-detect tooltip (works both inside and outside measure mode) */}
      {hoverInfo && (
        <MeasurementLabel
          position={hoverInfo.position}
          text={hoverInfo.type === 'hole'
            ? `Ø ${hoverInfo.value.toFixed(1)}`
            : `${hoverInfo.value.toFixed(1)} mm`}
          variant={hoverInfo.type === 'hole' ? 'accent' : 'primary'}
          fixed
        />
      )}

      {/* Pending first point (always in world coords) */}
      {measurePoints.length === 1 && (
        <PointMarker position={[measurePoints[0].x, measurePoints[0].y, measurePoints[0].z]} color="#e8a838" />
      )}

      {/* Legacy world-frame measurements (no modelId) */}
      {measurements.filter((m) => !m.modelId).map((m) => (
        <MeasurementGraphic key={m.id} m={m} />
      ))}
    </>
  );
}


/** Smart vertex snap on pointer move */
function SmartSnapHandler() {
  const { camera, scene, gl } = useThree();
  const measureMode = useModelStore((s) => s.measureMode);
  const setSnapPoint = useModelStore((s) => s.setSnapPoint);
  const raycaster = useMemo(() => new THREE.Raycaster(), []);
  const mouse = useMemo(() => new THREE.Vector2(), []);
  const mouseRef = useRef({ x: 0, y: 0 });

  useEffect(() => {
    const onMove = (e: MouseEvent) => {
      const rect = gl.domElement.getBoundingClientRect();
      mouseRef.current.x = e.clientX - rect.left;
      mouseRef.current.y = e.clientY - rect.top;
      mouse.x = (mouseRef.current.x / rect.width) * 2 - 1;
      mouse.y = -(mouseRef.current.y / rect.height) * 2 + 1;
    };
    gl.domElement.addEventListener('pointermove', onMove);
    return () => gl.domElement.removeEventListener('pointermove', onMove);
  }, [gl, mouse]);

  useFrame(() => {
    if (!measureMode) {
      setSnapPoint(null);
      return;
    }

    const st = useModelStore.getState();

    // Highest priority: a live hover label pointing at a hole → snap to its center
    if (st.hoverInfo?.type === 'hole') {
      const [hx, hy, hz] = st.hoverInfo.position;
      setSnapPoint({ x: hx, y: hy, z: hz });
      return;
    }

    raycaster.setFromCamera(mouse, camera);
    const intersects = raycaster.intersectObjects(scene.children, true);
    const hit = intersects.find(i => {
      const obj = i.object;
      if (obj instanceof THREE.GridHelper) return false;
      if (obj instanceof THREE.Mesh && obj.geometry instanceof THREE.SphereGeometry) return false;
      if (obj instanceof THREE.Mesh && obj.geometry instanceof THREE.RingGeometry) return false;
      if (obj.userData.__edgeLine) return false;
      return obj instanceof THREE.Mesh;
    });

    const canvas = gl.domElement;
    const canvasW = canvas.clientWidth;
    const canvasH = canvas.clientHeight;

    // Cursor in screen px (mouse is in NDC [-1, 1])
    const cursorX = (mouse.x * 0.5 + 0.5) * canvasW;
    const cursorY = (-mouse.y * 0.5 + 0.5) * canvasH;

    // Snap to centers of saved hole measurements within 20 px on screen
    const holeSnapPxThreshold = 20;
    let bestHoleDist = Infinity;
    let bestHole: { x: number; y: number; z: number } | null = null;
    const proj = new THREE.Vector3();
    for (const m of st.measurements) {
      if (m.kind !== 'hole') continue;
      proj.set(m.pointA.x, m.pointA.y, m.pointA.z).project(camera);
      const sx = (proj.x * 0.5 + 0.5) * canvasW;
      const sy = (-proj.y * 0.5 + 0.5) * canvasH;
      const d = Math.hypot(sx - cursorX, sy - cursorY);
      if (d < bestHoleDist && d <= holeSnapPxThreshold) {
        bestHoleDist = d;
        bestHole = { x: m.pointA.x, y: m.pointA.y, z: m.pointA.z };
      }
    }
    if (bestHole) {
      setSnapPoint(bestHole);
      return;
    }

    if (hit && hit.object instanceof THREE.Mesh) {
      const snap = findNearestVertex(
        hit.object, hit.point, camera,
        { width: canvasW, height: canvasH },
        10
      );
      if (snap) {
        setSnapPoint({ x: snap.x, y: snap.y, z: snap.z });
      } else {
        setSnapPoint(null);
      }
    } else {
      setSnapPoint(null);
    }
  });

  return null;
}

/** Hover detector for auto-detect hole diameter and edge length.
 *  Runs in BOTH view mode and measure mode. Outside measure mode the label
 *  auto-hides after 4s; in measure mode it persists until the user clicks
 *  (registering it as a saved measurement) or moves the mouse significantly. */
function HoverDetector() {
  const { camera, scene, gl } = useThree();
  const setHoverInfo = useModelStore((s) => s.setHoverInfo);
  const raycaster = useMemo(() => new THREE.Raycaster(), []);
  const mouse = useMemo(() => new THREE.Vector2(), []);
  const hoverTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
  const hideTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
  const lastHitKey = useRef('');

  useEffect(() => {
    const clearTimers = () => {
      if (hoverTimer.current) { clearTimeout(hoverTimer.current); hoverTimer.current = null; }
      if (hideTimer.current) { clearTimeout(hideTimer.current); hideTimer.current = null; }
    };

    const onMove = (e: MouseEvent) => {
      const rect = gl.domElement.getBoundingClientRect();
      mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
      mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;

      raycaster.setFromCamera(mouse, camera);
      const intersects = raycaster.intersectObjects(scene.children, true);
      const hit = intersects.find(i => {
        const obj = i.object;
        if (obj instanceof THREE.GridHelper) return false;
        if (obj instanceof THREE.Mesh && obj.geometry instanceof THREE.SphereGeometry) return false;
        if (obj instanceof THREE.Mesh && obj.geometry instanceof THREE.RingGeometry) return false;
        if (obj.userData.__edgeLine) return false;
        return obj instanceof THREE.Mesh;
      });

      if (!hit || !(hit.object instanceof THREE.Mesh)) {
        lastHitKey.current = '';
        setHoverInfo(null);
        clearTimers();
        return;
      }

      // Stability check – same approximate position for debounce (~3 mm)
      const key = `${hit.point.x.toFixed(3)},${hit.point.y.toFixed(3)},${hit.point.z.toFixed(3)}`;
      if (key === lastHitKey.current) return; // timer already running
      lastHitKey.current = key;
      setHoverInfo(null);
      clearTimers();

      hoverTimer.current = setTimeout(() => {
        if (!(hit.object instanceof THREE.Mesh)) return;
        const canvas = gl.domElement;
        const canvasSize = { width: canvas.clientWidth, height: canvas.clientHeight };

        // 1) Try circle fit from edge vertices around the hit (catches holes on flat faces)
        const circ = detectCircularEdgeAtPoint(hit.object, hit.point, camera, canvasSize, 50);
        if (circ) {
          setHoverInfo({
            type: 'hole',
            position: [circ.center.x, circ.center.y, circ.center.z],
            value: circ.diameterMM,
          });
        } else if (hit.faceIndex !== undefined && (() => {
          const hole = detectHoleAtFace(hit.object, hit.faceIndex);
          if (hole) {
            setHoverInfo({
              type: 'hole',
              position: [hole.center.x, hole.center.y, hole.center.z],
              value: hole.diameterMM,
            });
            return true;
          }
          return false;
        })()) {
          // handled inside IIFE
        } else {
          // 3) Edge length
          const edge = findNearestEdgeSegment(hit.object, hit.point, camera, canvasSize, 15);
          if (edge) {
            setHoverInfo({
              type: 'edge',
              position: [edge.midpoint.x, edge.midpoint.y, edge.midpoint.z],
              value: edge.lengthMM,
              endpoints: {
                a: { x: edge.a.x, y: edge.a.y, z: edge.a.z },
                b: { x: edge.b.x, y: edge.b.y, z: edge.b.z },
              },
            });
          } else {
            setHoverInfo(null);
            return;
          }
        }

        // Outside measure mode, auto-hide after 4 s
        const inMeasure = useModelStore.getState().measureMode;
        if (!inMeasure) {
          hideTimer.current = setTimeout(() => setHoverInfo(null), 4000);
        }
      }, 1000);
    };

    const onLeave = () => {
      lastHitKey.current = '';
      setHoverInfo(null);
      clearTimers();
    };

    gl.domElement.addEventListener('pointermove', onMove);
    gl.domElement.addEventListener('pointerleave', onLeave);
    return () => {
      gl.domElement.removeEventListener('pointermove', onMove);
      gl.domElement.removeEventListener('pointerleave', onLeave);
      clearTimers();
    };
  }, [camera, scene, gl, raycaster, mouse, setHoverInfo]);

  return null;
}

/** Pointer handler for measurement mode – uses snap point when available.
 *  Uses pointerdown/up with a drag gate so accidental drags don't add points. */
function MeasureClickHandler() {
  const { camera, scene, gl } = useThree();
  const measureMode = useModelStore((s) => s.measureMode);
  const raycaster = useMemo(() => new THREE.Raycaster(), []);
  const mouse = useMemo(() => new THREE.Vector2(), []);

  useEffect(() => {
    if (!measureMode) {
      gl.domElement.style.cursor = 'grab';
      return;
    }
    gl.domElement.style.cursor = 'crosshair';

    const canvas = gl.domElement;
    let downX = 0, downY = 0, downBtn = 0;
    let armed = false;

    const onDown = (e: PointerEvent) => {
      if (e.button !== 0) { armed = false; return; }
      downX = e.clientX;
      downY = e.clientY;
      downBtn = e.button;
      armed = true;
    };

    const onUp = (e: PointerEvent) => {
      if (!armed || e.button !== downBtn) return;
      armed = false;
      const dx = e.clientX - downX;
      const dy = e.clientY - downY;
      if (Math.hypot(dx, dy) > 4) return; // it was a drag, not a click

      const st = useModelStore.getState();
      if (!st.measureMode) return;

      // If a hover label is currently shown (hole diameter or edge length),
      // a click registers it as a saved measurement.
      if (st.hoverInfo) {
        st.registerHoverMeasurement(st.hoverInfo);
        return;
      }

      // Prefer snap (vertex snap, or hole-center snap from SmartSnapHandler)
      if (st.snapPoint) {
        st.addMeasurePoint({ x: st.snapPoint.x, y: st.snapPoint.y, z: st.snapPoint.z });
        return;
      }

      const rect = canvas.getBoundingClientRect();
      mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
      mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
      raycaster.setFromCamera(mouse, camera);
      const intersects = raycaster.intersectObjects(scene.children, true);
      const hit = intersects.find(i => {
        const obj = i.object;
        if (!(obj instanceof THREE.Mesh)) return false;
        if (obj.userData.__edgeLine) return false;
        if (obj.geometry instanceof THREE.SphereGeometry) return false;
        if (obj.geometry instanceof THREE.RingGeometry) return false;
        if (obj.geometry instanceof THREE.PlaneGeometry) return false;
        return true;
      });
      if (hit) {
        st.addMeasurePoint({ x: hit.point.x, y: hit.point.y, z: hit.point.z });
      }
    };

    const onKey = (e: KeyboardEvent) => {
      const st = useModelStore.getState();
      if (!st.measureMode) return;
      if (e.key === 'Escape') {
        if (st.measurePoints.length > 0) st.undoLastMeasurePoint();
      } else if ((e.key === 'z' || e.key === 'Z') && !e.ctrlKey && !e.metaKey) {
        st.undoLastMeasurement();
      }
    };

    canvas.addEventListener('pointerdown', onDown);
    canvas.addEventListener('pointerup', onUp);
    window.addEventListener('keydown', onKey);
    return () => {
      canvas.removeEventListener('pointerdown', onDown);
      canvas.removeEventListener('pointerup', onUp);
      window.removeEventListener('keydown', onKey);
      gl.domElement.style.cursor = 'grab';
    };
  }, [measureMode, camera, scene, gl, raycaster, mouse]);

  return null;
}


/** Pointer handler for element-selection mode. Clicks an IFC element, sub-mesh
 *  of a non-IFC model, or any pickable mesh group. Toggles selection in store. */
function SelectionHandler() {
  const { camera, scene, gl } = useThree();
  const selectionMode = useModelStore((s) => s.selectionMode);
  const raycaster = useMemo(() => new THREE.Raycaster(), []);
  const mouse = useMemo(() => new THREE.Vector2(), []);

  useEffect(() => {
    if (!selectionMode) return;
    const prevCursor = gl.domElement.style.cursor;
    gl.domElement.style.cursor = 'cell';
    const canvas = gl.domElement;

    const onR3FMiss = (event: Event) => {
      const detail = (event as CustomEvent<PointerEvent>).detail;
      if (!detail || detail.button !== 0) return;
      const rect = canvas.getBoundingClientRect();
      mouse.x = ((detail.clientX - rect.left) / rect.width) * 2 - 1;
      mouse.y = -((detail.clientY - rect.top) / rect.height) * 2 + 1;
      raycaster.setFromCamera(mouse, camera);
      const intersects = raycaster.intersectObjects(scene.children, true);
      const hit = intersects.find((i) => isPickableModelMesh(i.object));
      if (!hit) return;

      const element = findElementRoot(hit.object);
      const modelId = useModelStore.getState().activeModelId;
      if (!modelId || !element) return;
      useModelStore.getState().toggleElementSelection(elementKey(modelId, element));
    };

    canvas.addEventListener('tsxr-selection-miss', onR3FMiss);
    return () => {
      canvas.removeEventListener('tsxr-selection-miss', onR3FMiss);
      gl.domElement.style.cursor = prevCursor;
    };
  }, [selectionMode, camera, scene, gl, raycaster, mouse]);

  return null;
}

/** Walks the scene each time visibility/selection state changes and applies
 *  per-element visibility + emissive highlight for selected elements. */
export function VisibilityApplier() {
  const { scene } = useThree();
  const nonce = useModelStore((s) => s.visibilityNonce);
  const models = useModelStore((s) => s.models);
  const opacity = useModelStore((s) => s.opacity);
  const renderMode = useModelStore((s) => s.renderMode);
  const checklist = useModelStore((s) => s.checklist);

  useEffect(() => {
    const st = useModelStore.getState();
    const { hiddenElementKeys, isolatedElementKeys, selectedElementKeys } = st;

    scene.traverse((root) => {
      const modelId = root.userData?.modelId as string | undefined;
      if (!modelId) return;
      root.traverse((el) => {
        if (!el.userData?.ifcElement && !(isPickableModelMesh(el) && !hasIfcAncestor(el))) return;
        const key = elementKey(modelId, el);
        const hidden = hiddenElementKeys.has(key);
        const isolated = isolatedElementKeys ? !isolatedElementKeys.has(key) : false;
        el.visible = !(hidden || isolated);

        const selected = selectedElementKeys.has(key);
        el.traverse((m) => {
          if (!(m instanceof THREE.Mesh)) return;
          if (selected) {
            if (!m.userData.__origMaterial) {
              m.userData.__origMaterial = m.material;
              const src = Array.isArray(m.material) ? m.material[0] : m.material;
              const cloned = (src as THREE.Material).clone() as THREE.MeshStandardMaterial;
              if (cloned instanceof THREE.MeshStandardMaterial) {
                cloned.color.set('#f59e0b');
                cloned.emissive.set('#f59e0b');
                cloned.emissiveIntensity = 0.8;
                cloned.needsUpdate = true;
              }
              m.material = cloned;
            }
          } else if (m.userData.__origMaterial) {
            // Dispose the cloned highlight material
            const cur = Array.isArray(m.material) ? m.material[0] : m.material;
            (cur as THREE.Material)?.dispose?.();
            m.material = m.userData.__origMaterial as THREE.Material;
            delete m.userData.__origMaterial;
          }
        });
      });
    });
  }, [scene, nonce, models, opacity, renderMode, checklist]);

  return null;
}


function GridLayer() {
  const showGrid = useModelStore((s) => s.showGrid);
  const gridY = useModelStore((s) => s.gridY);
  if (!showGrid) return null;
  return (
    <Grid
      position={[0, gridY, 0]}
      infiniteGrid
      cellSize={0.01}
      sectionSize={0.1}
      cellThickness={0.5}
      sectionThickness={1}
      cellColor="#334155"
      sectionColor="#475569"
      fadeDistance={5}
      fadeStrength={1}
    />
  );
}

/** Computes min Y of all visible models and updates store gridY when auto-follow is on. */
function GridAutoFollower() {
  const models = useModelStore((s) => s.models);
  const { scene } = useThree();
  useEffect(() => {
    const auto = useModelStore.getState().gridAutoFollow;
    if (!auto) return;
    // Defer to next tick so models have rendered/positioned
    const id = setTimeout(() => {
      if (!useModelStore.getState().gridAutoFollow) return;
      const box = new THREE.Box3();
      let has = false;
      scene.traverse((obj) => {
        if (obj instanceof THREE.Mesh && obj.geometry) {
          // Skip helpers (markers, rings)
          if (obj.geometry instanceof THREE.SphereGeometry) return;
          if (obj.geometry instanceof THREE.RingGeometry) return;
          if (obj.userData.__edgeLine) return;
          obj.updateWorldMatrix(true, false);
          const b = new THREE.Box3().setFromObject(obj);
          if (isFinite(b.min.y)) {
            if (!has) { box.copy(b); has = true; }
            else box.union(b);
          }
        }
      });
      if (has) {
        useModelStore.setState({ gridY: box.min.y - 0.005 });
      }
    }, 100);
    return () => clearTimeout(id);
  }, [models, scene]);
  return null;
}

/** Mouse drag handler for desktop "Posicionar" mode: translates/rotates the active model. */
function PositionDragHandler() {
  const { camera, gl, scene } = useThree();
  const positionMode = useModelStore((s) => s.positionMode);
  const activeId = useModelStore((s) => s.activeModelId);

  useEffect(() => {
    if (!positionMode) {
      gl.domElement.style.cursor = 'grab';
      return;
    }
    gl.domElement.style.cursor = 'move';

    const canvas = gl.domElement;
    let dragging = false;
    let button = 0;
    let shiftKey = false;
    let lastX = 0;
    let lastY = 0;
    let pixelsPerWorldUnit = 1;

    const computePixelScale = () => {
      const st = useModelStore.getState();
      const active = st.models.find((m) => m.id === st.activeModelId);
      if (!active) return;
      // Find the model's group position (approx world): use renderFactor + fineTuning pos
      const factor = st.scaleRatio?.factor ?? 1;
      const wp = new THREE.Vector3(
        active.fineTuning.posX * factor,
        active.fineTuning.posY * factor,
        active.fineTuning.posZ * factor,
      );
      const dist = camera.position.distanceTo(wp);
      const perspCam = camera as THREE.PerspectiveCamera;
      const fov = (perspCam.fov ?? 50) * (Math.PI / 180);
      const worldPerPixel = (2 * dist * Math.tan(fov / 2)) / canvas.clientHeight;
      pixelsPerWorldUnit = worldPerPixel; // world units per pixel
    };

    const onDown = (e: PointerEvent) => {
      const st = useModelStore.getState();
      if (!st.positionMode || !st.activeModelId) return;
      dragging = true;
      button = e.button;
      shiftKey = e.shiftKey;
      lastX = e.clientX;
      lastY = e.clientY;
      computePixelScale();
      (e.target as Element).setPointerCapture?.(e.pointerId);
      e.preventDefault();
    };

    const onMove = (e: PointerEvent) => {
      if (!dragging) return;
      const st = useModelStore.getState();
      const active = st.models.find((m) => m.id === st.activeModelId);
      if (!active) return;

      const dx = e.clientX - lastX;
      const dy = e.clientY - lastY;
      lastX = e.clientX;
      lastY = e.clientY;
      shiftKey = e.shiftKey;

      const ft = active.fineTuning;
      const factor = st.scaleRatio?.factor ?? 1;

      if (button === 2) {
        // Right button: rotate
        const sens = 0.4; // deg per pixel
        if (shiftKey) {
          useModelStore.getState().setFineTuning({ rotZ: ft.rotZ + dx * sens });
        } else {
          useModelStore.getState().setFineTuning({
            rotY: ft.rotY + dx * sens,
            rotX: ft.rotX + dy * sens,
          });
        }
      } else if (button === 1) {
        // Middle button (wheel): roll around the piece's own longitudinal axis
        const sens = 0.5;
        let dominant: 'x' | 'y' | 'z' = 'x';
        scene.traverse((o) => {
          if (o.userData?.modelId === active.id && o.userData?.dominantAxis) {
            dominant = o.userData.dominantAxis;
          }
        });
        const delta = dx * sens;
        if (dominant === 'x') {
          useModelStore.getState().setFineTuning({ rotX: ft.rotX + delta });
        } else if (dominant === 'y') {
          useModelStore.getState().setFineTuning({ rotY: ft.rotY + delta });
        } else {
          useModelStore.getState().setFineTuning({ rotZ: ft.rotZ + delta });
        }
      } else if (button === 0 && shiftKey) {
        // Shift+left: depth (Z in camera space)
        const worldDelta = dy * pixelsPerWorldUnit;
        const camDir = new THREE.Vector3();
        camera.getWorldDirection(camDir);
        const move = camDir.multiplyScalar(worldDelta).divideScalar(factor);
        useModelStore.getState().setFineTuning({
          posX: ft.posX + move.x,
          posY: ft.posY + move.y,
          posZ: ft.posZ + move.z,
        });
      } else {
        // Left: translate in camera plane
        const right = new THREE.Vector3();
        const up = new THREE.Vector3();
        camera.matrixWorld.extractBasis(right, up, new THREE.Vector3());
        const worldDX = dx * pixelsPerWorldUnit;
        const worldDY = -dy * pixelsPerWorldUnit;
        const move = right.multiplyScalar(worldDX).add(up.multiplyScalar(worldDY)).divideScalar(factor);
        useModelStore.getState().setFineTuning({
          posX: ft.posX + move.x,
          posY: ft.posY + move.y,
          posZ: ft.posZ + move.z,
        });
      }
    };

    const onUp = (e: PointerEvent) => {
      dragging = false;
      (e.target as Element).releasePointerCapture?.(e.pointerId);
    };

    const onContext = (e: MouseEvent) => e.preventDefault();

    canvas.addEventListener('pointerdown', onDown);
    canvas.addEventListener('pointermove', onMove);
    canvas.addEventListener('pointerup', onUp);
    canvas.addEventListener('pointercancel', onUp);
    canvas.addEventListener('contextmenu', onContext);
    return () => {
      canvas.removeEventListener('pointerdown', onDown);
      canvas.removeEventListener('pointermove', onMove);
      canvas.removeEventListener('pointerup', onUp);
      canvas.removeEventListener('pointercancel', onUp);
      canvas.removeEventListener('contextmenu', onContext);
      gl.domElement.style.cursor = 'grab';
    };
  }, [positionMode, activeId, camera, gl, scene]);

  return null;
}



export function ModelViewerCanvas({ url }: ModelViewerProps) {
  const positionMode = useModelStore((s) => s.positionMode);
  const measureMode = useModelStore((s) => s.measureMode);
  const selectionMode = useModelStore((s) => s.selectionMode);
  return (
    <Canvas
      camera={{ position: [2, 2, 2], fov: 50, near: 0.001, far: 1000 }}
      gl={{ antialias: true, alpha: true, powerPreference: 'high-performance', preserveDrawingBuffer: true }}
      frameloop="always"
      className="!bg-background"
      onPointerMissed={(e) => {
        if (useModelStore.getState().selectionMode) {
          (e.target as HTMLCanvasElement).dispatchEvent(new CustomEvent('tsxr-selection-miss', { detail: e }));
        }
      }}
      onCreated={({ gl }) => {
        const isApple = /Mac|iPhone|iPad/.test(navigator.platform);
        const maxRatio = isApple ? 2 : 1.5;
        gl.setPixelRatio(Math.min(window.devicePixelRatio, maxRatio));
      }}
    >
      <CameraSwitcher />
      <ambientLight intensity={0.6} />
      <directionalLight position={[5, 10, 5]} intensity={1} castShadow />
      <directionalLight position={[-5, 5, -5]} intensity={0.3} />
      
      <Suspense fallback={<LoadingFallback />}>
        <SceneModels />
      </Suspense>

      <MeasurementOverlay />
      <MeasureClickHandler />
      <SmartSnapHandler />
      <HoverDetector />

      <GridLayer />
      <GridAutoFollower />

      <PositionDragHandler />
      <SelectionHandler />
      <VisibilityApplier />
      <SceneRefCapture />
      <ViewCubeAnimator />

      <OrbitControls
        makeDefault
        enableDamping
        dampingFactor={0.1}
        minDistance={0.05}
        maxDistance={50}
        minZoom={5}
        maxZoom={5000}
        enabled={!positionMode}
        ref={(c: any) => {
          mainControlsRef.current = c;
        }}
      />


    </Canvas>
  );
}
