import * as THREE from 'three';

const _v = new THREE.Vector3();
const _projected = new THREE.Vector3();

/**
 * Find the nearest vertex in a mesh to a world-space point,
 * returning it only if it's within `thresholdPx` pixels on screen.
 */
export function findNearestVertex(
  mesh: THREE.Mesh,
  worldPoint: THREE.Vector3,
  camera: THREE.Camera,
  canvasSize: { width: number; height: number },
  thresholdPx: number = 10
): THREE.Vector3 | null {
  const geo = mesh.geometry;
  const posAttr = geo.attributes.position;
  if (!posAttr) return null;

  // Project hit point to screen
  const hitScreen = worldPoint.clone().project(camera);
  const hitX = (hitScreen.x * 0.5 + 0.5) * canvasSize.width;
  const hitY = (-hitScreen.y * 0.5 + 0.5) * canvasSize.height;

  let bestDist = Infinity;
  let bestVertex: THREE.Vector3 | null = null;

  for (let i = 0; i < posAttr.count; i++) {
    _v.fromBufferAttribute(posAttr, i);
    _v.applyMatrix4(mesh.matrixWorld);

    _projected.copy(_v).project(camera);
    const sx = (_projected.x * 0.5 + 0.5) * canvasSize.width;
    const sy = (-_projected.y * 0.5 + 0.5) * canvasSize.height;

    const dx = sx - hitX;
    const dy = sy - hitY;
    const dist = Math.sqrt(dx * dx + dy * dy);

    if (dist < bestDist) {
      bestDist = dist;
      bestVertex = _v.clone();
    }
  }

  if (bestDist <= thresholdPx && bestVertex) {
    return bestVertex;
  }
  return null;
}

/**
 * Detect if the face under the cursor belongs to a cylindrical hole.
 * Uses face normals around the hit to detect radial patterns → circle fit.
 * Returns diameter in mm, or null if not a hole.
 */
export function detectHoleAtFace(
  mesh: THREE.Mesh,
  faceIndex: number
): { center: THREE.Vector3; diameterMM: number } | null {
  const geo = mesh.geometry;
  const posAttr = geo.attributes.position;
  const normalAttr = geo.attributes.normal;
  const indexAttr = geo.index;

  if (!posAttr || !normalAttr || !indexAttr) return null;

  // Get hit face normal
  const i0 = indexAttr.getX(faceIndex * 3);
  const i1 = indexAttr.getX(faceIndex * 3 + 1);
  const i2 = indexAttr.getX(faceIndex * 3 + 2);

  const hitNormal = new THREE.Vector3();
  const n0 = new THREE.Vector3().fromBufferAttribute(normalAttr, i0);
  const n1 = new THREE.Vector3().fromBufferAttribute(normalAttr, i1);
  const n2 = new THREE.Vector3().fromBufferAttribute(normalAttr, i2);
  hitNormal.addVectors(n0, n1).add(n2).normalize();

  const hitCenter = new THREE.Vector3();
  const p0 = new THREE.Vector3().fromBufferAttribute(posAttr, i0);
  const p1 = new THREE.Vector3().fromBufferAttribute(posAttr, i1);
  const p2 = new THREE.Vector3().fromBufferAttribute(posAttr, i2);
  hitCenter.addVectors(p0, p1).add(p2).divideScalar(3);

  // Collect neighboring faces with similar "cylindrical" normal pattern
  // Cylindrical faces have normals perpendicular to the cylinder axis
  // We look for faces whose normals are roughly perpendicular to each other
  // but share a common axis (the hole axis)

  const faceCount = indexAttr.count / 3;
  const cylinderVertices: THREE.Vector3[] = [];
  const faceNormals: THREE.Vector3[] = [];

  // Threshold for proximity (local search)
  const searchRadius = 0.05; // 50mm in model units

  for (let f = 0; f < faceCount; f++) {
    const fi0 = indexAttr.getX(f * 3);
    const fi1 = indexAttr.getX(f * 3 + 1);
    const fi2 = indexAttr.getX(f * 3 + 2);

    const fp0 = new THREE.Vector3().fromBufferAttribute(posAttr, fi0);
    const fp1 = new THREE.Vector3().fromBufferAttribute(posAttr, fi1);
    const fp2 = new THREE.Vector3().fromBufferAttribute(posAttr, fi2);

    const fc = new THREE.Vector3().addVectors(fp0, fp1).add(fp2).divideScalar(3);
    if (fc.distanceTo(hitCenter) > searchRadius) continue;

    const fn = new THREE.Vector3();
    const fn0 = new THREE.Vector3().fromBufferAttribute(normalAttr, fi0);
    const fn1 = new THREE.Vector3().fromBufferAttribute(normalAttr, fi1);
    const fn2 = new THREE.Vector3().fromBufferAttribute(normalAttr, fi2);
    fn.addVectors(fn0, fn1).add(fn2).normalize();

    // For cylindrical surfaces, normals should be roughly perpendicular to hole axis
    // and the dot product between hit normal and face normal reveals if they share curvature
    const dot = Math.abs(hitNormal.dot(fn));
    // Cylindrical faces: normals vary (dot < 0.95) but aren't opposite (dot > -0.5)
    if (dot < 0.98) {
      cylinderVertices.push(fp0, fp1, fp2);
      faceNormals.push(fn);
    }
  }

  if (cylinderVertices.length < 9) return null; // Need at least 3 faces

  // Try to find a common axis and fit a circle
  // Estimate axis as cross product of two differing normals
  let axis: THREE.Vector3 | null = null;
  for (let i = 1; i < faceNormals.length; i++) {
    const cross = new THREE.Vector3().crossVectors(faceNormals[0], faceNormals[i]);
    if (cross.length() > 0.1) {
      axis = cross.normalize();
      break;
    }
  }
  if (!axis) return null;

  // Project vertices onto plane perpendicular to axis
  // Use least-squares circle fit on 2D projections
  const basisU = new THREE.Vector3();
  const basisV = new THREE.Vector3();
  
  // Create orthonormal basis
  if (Math.abs(axis.x) < 0.9) {
    basisU.crossVectors(axis, new THREE.Vector3(1, 0, 0)).normalize();
  } else {
    basisU.crossVectors(axis, new THREE.Vector3(0, 1, 0)).normalize();
  }
  basisV.crossVectors(axis, basisU).normalize();

  // Project unique vertices to 2D
  const seen = new Set<string>();
  const points2D: { u: number; v: number }[] = [];
  
  for (const vert of cylinderVertices) {
    const key = `${vert.x.toFixed(6)},${vert.y.toFixed(6)},${vert.z.toFixed(6)}`;
    if (seen.has(key)) continue;
    seen.add(key);
    
    const rel = vert.clone().sub(hitCenter);
    points2D.push({
      u: rel.dot(basisU),
      v: rel.dot(basisV),
    });
  }

  if (points2D.length < 4) return null;

  // Least-squares circle fit (Kasa method)
  const result = circleFitKasa(points2D);
  if (!result) return null;

  const radiusModel = result.radius;
  const diameterMM = radiusModel * 2 * 1000;

  // Filter: reasonable hole sizes (2mm to 200mm diameter)
  if (diameterMM < 2 || diameterMM > 200) return null;

  // Reconstruct 3D center
  const center3D = hitCenter.clone()
    .add(basisU.clone().multiplyScalar(result.cx))
    .add(basisV.clone().multiplyScalar(result.cy));

  center3D.applyMatrix4(mesh.matrixWorld);

  return { center: center3D, diameterMM };
}

/** Kasa circle fit: returns { cx, cy, radius } in the input coordinate system */
export function circleFitKasa(points: { u: number; v: number }[]): { cx: number; cy: number; radius: number } | null {
  const n = points.length;
  if (n < 3) return null;

  let su = 0, sv = 0, suu = 0, svv = 0, suv = 0, suuu = 0, svvv = 0, suvv = 0, svuu = 0;
  
  for (const p of points) {
    su += p.u; sv += p.v;
    suu += p.u * p.u; svv += p.v * p.v;
    suv += p.u * p.v;
    suuu += p.u * p.u * p.u; svvv += p.v * p.v * p.v;
    suvv += p.u * p.v * p.v; svuu += p.v * p.u * p.u;
  }

  const A = n * suu - su * su;
  const B = n * suv - su * sv;
  const C = n * svv - sv * sv;
  const D = 0.5 * (n * suuu + n * suvv - su * suu - su * svv);
  const E = 0.5 * (n * svvv + n * svuu - sv * suu - sv * svv);

  const denom = A * C - B * B;
  if (Math.abs(denom) < 1e-12) return null;

  const cx = (D * C - E * B) / denom;
  const cy = (A * E - B * D) / denom;

  let r2sum = 0;
  for (const p of points) {
    r2sum += (p.u - cx) ** 2 + (p.v - cy) ** 2;
  }
  const radius = Math.sqrt(r2sum / n);

  // Check fit quality: standard deviation of radii should be small relative to radius
  let variance = 0;
  for (const p of points) {
    const r = Math.sqrt((p.u - cx) ** 2 + (p.v - cy) ** 2);
    variance += (r - radius) ** 2;
  }
  const stdDev = Math.sqrt(variance / n);
  if (stdDev / radius > 0.15) return null; // Poor fit, not a circle

  return { cx, cy, radius };
}

/**
 * Find nearest edge segment from EdgesGeometry data.
 * Returns distance in mm or null.
 */
export function findNearestEdgeSegment(
  mesh: THREE.Mesh,
  worldPoint: THREE.Vector3,
  camera: THREE.Camera,
  canvasSize: { width: number; height: number },
  thresholdPx: number = 12
): { midpoint: THREE.Vector3; lengthMM: number; a: THREE.Vector3; b: THREE.Vector3 } | null {
  // Look for edge line segments children
  let edgeLines: THREE.LineSegments | null = null;
  mesh.children.forEach(c => {
    if (c.userData.__edgeLine && c instanceof THREE.LineSegments) {
      edgeLines = c;
    }
  });

  // If no edge lines, fetch from cache or generate and store in cache
  let geo: THREE.BufferGeometry;
  if (edgeLines) {
    geo = edgeLines.geometry;
  } else {
    if (!mesh.userData.__cachedEdgesGeometry) {
      mesh.userData.__cachedEdgesGeometry = new THREE.EdgesGeometry(mesh.geometry, 15);
    }
    geo = mesh.userData.__cachedEdgesGeometry;
  }
  const posAttr = geo.attributes.position;
  if (!posAttr) return null;

  // Project hit point to screen
  const hitScreen = worldPoint.clone().project(camera);
  const hitX = (hitScreen.x * 0.5 + 0.5) * canvasSize.width;
  const hitY = (-hitScreen.y * 0.5 + 0.5) * canvasSize.height;

  const matrix = edgeLines ? edgeLines.matrixWorld : mesh.matrixWorld;

  let bestDist = Infinity;
  let bestMid: THREE.Vector3 | null = null;
  let bestA: THREE.Vector3 | null = null;
  let bestB: THREE.Vector3 | null = null;
  let bestLen = 0;

  const segCount = posAttr.count / 2;
  const a = new THREE.Vector3();
  const b = new THREE.Vector3();
  const pa = new THREE.Vector3();
  const pb = new THREE.Vector3();

  for (let i = 0; i < segCount; i++) {
    a.fromBufferAttribute(posAttr, i * 2).applyMatrix4(matrix);
    b.fromBufferAttribute(posAttr, i * 2 + 1).applyMatrix4(matrix);

    pa.copy(a).project(camera);
    pb.copy(b).project(camera);
    const ax = (pa.x * 0.5 + 0.5) * canvasSize.width;
    const ay = (-pa.y * 0.5 + 0.5) * canvasSize.height;
    const bx = (pb.x * 0.5 + 0.5) * canvasSize.width;
    const by = (-pb.y * 0.5 + 0.5) * canvasSize.height;

    const dist = pointToSegmentDist(hitX, hitY, ax, ay, bx, by);

    if (dist < bestDist) {
      bestDist = dist;
      bestMid = a.clone().add(b).multiplyScalar(0.5);
      bestA = a.clone();
      bestB = b.clone();
      bestLen = a.distanceTo(b) * 1000; // mm
    }
  }

  if (bestDist <= thresholdPx && bestMid && bestA && bestB && bestLen > 0.5) {
    return { midpoint: bestMid, lengthMM: bestLen, a: bestA, b: bestB };
  }
  return null;
}

/**
 * Detect a circular hole by fitting a circle through edge vertices that lie
 * within `radiusPx` (screen space) around `worldPoint`. Returns center + diameter.
 */
export function detectCircularEdgeAtPoint(
  mesh: THREE.Mesh,
  worldPoint: THREE.Vector3,
  camera: THREE.Camera,
  canvasSize: { width: number; height: number },
  radiusPx: number = 60
): { center: THREE.Vector3; diameterMM: number } | null {
  let edgeLines: THREE.LineSegments | null = null;
  mesh.children.forEach(c => {
    if (c.userData.__edgeLine && c instanceof THREE.LineSegments) edgeLines = c;
  });

  let geo: THREE.BufferGeometry;
  if (edgeLines) {
    geo = edgeLines.geometry;
  } else {
    if (!mesh.userData.__cachedEdgesGeometry) {
      mesh.userData.__cachedEdgesGeometry = new THREE.EdgesGeometry(mesh.geometry, 15);
    }
    geo = mesh.userData.__cachedEdgesGeometry;
  }
  const posAttr = geo.attributes.position;
  if (!posAttr) return null;

  const matrix = edgeLines ? edgeLines.matrixWorld : mesh.matrixWorld;

  const hitScreen = worldPoint.clone().project(camera);
  const hitX = (hitScreen.x * 0.5 + 0.5) * canvasSize.width;
  const hitY = (-hitScreen.y * 0.5 + 0.5) * canvasSize.height;

  const verts: THREE.Vector3[] = [];
  const seen = new Set<string>();
  const tmp = new THREE.Vector3();
  const proj = new THREE.Vector3();
  const count = posAttr.count;
  for (let i = 0; i < count; i++) {
    tmp.fromBufferAttribute(posAttr, i).applyMatrix4(matrix);
    proj.copy(tmp).project(camera);
    const sx = (proj.x * 0.5 + 0.5) * canvasSize.width;
    const sy = (-proj.y * 0.5 + 0.5) * canvasSize.height;
    if (Math.hypot(sx - hitX, sy - hitY) > radiusPx) continue;
    const key = `${tmp.x.toFixed(5)},${tmp.y.toFixed(5)},${tmp.z.toFixed(5)}`;
    if (seen.has(key)) continue;
    seen.add(key);
    verts.push(tmp.clone());
  }

  if (verts.length < 6) return null;

  // Compute centroid + best-fit plane normal via covariance
  const centroid = new THREE.Vector3();
  verts.forEach(v => centroid.add(v));
  centroid.divideScalar(verts.length);

  let xx = 0, xy = 0, xz = 0, yy = 0, yz = 0, zz = 0;
  for (const v of verts) {
    const dx = v.x - centroid.x, dy = v.y - centroid.y, dz = v.z - centroid.z;
    xx += dx * dx; xy += dx * dy; xz += dx * dz;
    yy += dy * dy; yz += dy * dz; zz += dz * dz;
  }
  // Normal = eigenvector with smallest eigenvalue. Approximate using cross of two
  // axes with largest variance.
  const axisA = new THREE.Vector3(xx, xy, xz).normalize();
  const axisB = new THREE.Vector3(xy, yy, yz).normalize();
  const normal = new THREE.Vector3().crossVectors(axisA, axisB);
  if (normal.lengthSq() < 1e-8) return null;
  normal.normalize();

  // Build basis on plane
  const basisU = new THREE.Vector3();
  if (Math.abs(normal.x) < 0.9) basisU.crossVectors(normal, new THREE.Vector3(1, 0, 0)).normalize();
  else basisU.crossVectors(normal, new THREE.Vector3(0, 1, 0)).normalize();
  const basisV = new THREE.Vector3().crossVectors(normal, basisU).normalize();

  const points2D = verts.map(v => {
    const rel = v.clone().sub(centroid);
    return { u: rel.dot(basisU), v: rel.dot(basisV) };
  });
  const fit = circleFitKasa(points2D);
  if (!fit) return null;

  const diameterMM = fit.radius * 2 * 1000;
  if (diameterMM < 2 || diameterMM > 500) return null;

  const center3D = centroid.clone()
    .add(basisU.clone().multiplyScalar(fit.cx))
    .add(basisV.clone().multiplyScalar(fit.cy));

  return { center: center3D, diameterMM };
}

function pointToSegmentDist(px: number, py: number, ax: number, ay: number, bx: number, by: number): number {
  const dx = bx - ax;
  const dy = by - ay;
  const lenSq = dx * dx + dy * dy;
  if (lenSq < 0.01) return Math.sqrt((px - ax) ** 2 + (py - ay) ** 2);

  let t = ((px - ax) * dx + (py - ay) * dy) / lenSq;
  t = Math.max(0, Math.min(1, t));
  const closestX = ax + t * dx;
  const closestY = ay + t * dy;
  return Math.sqrt((px - closestX) ** 2 + (py - closestY) ** 2);
}

/**
 * Combined snap resolver: tries vertex first, then edge midpoint,
 * falling back to the raw hit point. Returns { point, type }.
 */
export function resolveSnap(
  mesh: THREE.Mesh,
  hitPoint: THREE.Vector3,
  camera: THREE.Camera,
  canvasSize: { width: number; height: number },
  vertexThresholdPx: number = 12,
  edgeThresholdPx: number = 16
): { point: THREE.Vector3; type: 'vertex' | 'edge' | 'surface' } {
  const v = findNearestVertex(mesh, hitPoint, camera, canvasSize, vertexThresholdPx);
  if (v) return { point: v, type: 'vertex' };
  const e = findNearestEdgeSegment(mesh, hitPoint, camera, canvasSize, edgeThresholdPx);
  if (e) return { point: e.midpoint, type: 'edge' };
  return { point: hitPoint.clone(), type: 'surface' };
}

const _ab = new THREE.Vector3();
const _ap = new THREE.Vector3();
const _closest = new THREE.Vector3();

function closestPointOnSegment3D(p: THREE.Vector3, a: THREE.Vector3, b: THREE.Vector3, target: THREE.Vector3): THREE.Vector3 {
  _ab.subVectors(b, a);
  _ap.subVectors(p, a);
  const abLenSq = _ab.lengthSq();
  if (abLenSq < 1e-6) return target.copy(a);

  const t = THREE.MathUtils.clamp(_ap.dot(_ab) / abLenSq, 0, 1);
  return target.copy(a).addScaledVector(_ab, t);
}

function pointToSegmentDistSq3D(p: THREE.Vector3, a: THREE.Vector3, b: THREE.Vector3): number {
  _ab.subVectors(b, a);
  _ap.subVectors(p, a);
  const abLenSq = _ab.lengthSq();
  if (abLenSq < 1e-6) return _ap.lengthSq();

  let t = _ap.dot(_ab) / abLenSq;
  t = Math.max(0, Math.min(1, t));
  _closest.copy(a).addScaledVector(_ab, t);
  return p.distanceToSquared(_closest);
}

export function findNearestVertex3D(
  mesh: THREE.Mesh,
  worldPoint: THREE.Vector3,
  thresholdMeters: number = 0.035
): THREE.Vector3 | null {
  const geo = mesh.geometry;
  const posAttr = geo.attributes.position;
  if (!posAttr) return null;

  const invMatrix = new THREE.Matrix4().copy(mesh.matrixWorld).invert();
  const localPoint = worldPoint.clone().applyMatrix4(invMatrix);

  let bestDistSq = Infinity;
  let bestIndex = -1;
  const thresholdSq = thresholdMeters * thresholdMeters;

  for (let i = 0; i < posAttr.count; i++) {
    const vx = posAttr.getX(i);
    const vy = posAttr.getY(i);
    const vz = posAttr.getZ(i);

    const dx = vx - localPoint.x;
    const dy = vy - localPoint.y;
    const dz = vz - localPoint.z;
    const distSq = dx * dx + dy * dy + dz * dz;

    if (distSq < bestDistSq) {
      bestDistSq = distSq;
      bestIndex = i;
    }
  }

  if (bestIndex !== -1 && bestDistSq <= thresholdSq) {
    const result = new THREE.Vector3(
      posAttr.getX(bestIndex),
      posAttr.getY(bestIndex),
      posAttr.getZ(bestIndex)
    );
    result.applyMatrix4(mesh.matrixWorld);
    return result;
  }
  return null;
}

export function findNearestEdgeSegment3D(
  mesh: THREE.Mesh,
  worldPoint: THREE.Vector3,
  thresholdMeters: number = 0.05
): { midpoint: THREE.Vector3; snapPoint: THREE.Vector3; lengthMM: number; a: THREE.Vector3; b: THREE.Vector3 } | null {
  let edgeLines: THREE.LineSegments | null = null;
  mesh.children.forEach(c => {
    if (c.userData.__edgeLine && c instanceof THREE.LineSegments) {
      edgeLines = c;
    }
  });

  let geo: THREE.BufferGeometry;
  if (edgeLines) {
    geo = edgeLines.geometry;
  } else {
    if (!mesh.userData.__cachedEdgesGeometry) {
      mesh.userData.__cachedEdgesGeometry = new THREE.EdgesGeometry(mesh.geometry, 15);
    }
    geo = mesh.userData.__cachedEdgesGeometry;
  }
  const posAttr = geo.attributes.position;
  if (!posAttr) return null;

  const matrix = edgeLines ? edgeLines.matrixWorld : mesh.matrixWorld;
  const invMatrix = new THREE.Matrix4().copy(matrix).invert();
  const localPoint = worldPoint.clone().applyMatrix4(invMatrix);

  let bestDistSq = Infinity;
  let bestIndex = -1;
  const bestClosestLocal = new THREE.Vector3();
  const thresholdSq = thresholdMeters * thresholdMeters;

  const segCount = posAttr.count / 2;
  const aLocal = new THREE.Vector3();
  const bLocal = new THREE.Vector3();

  for (let i = 0; i < segCount; i++) {
    aLocal.fromBufferAttribute(posAttr, i * 2);
    bLocal.fromBufferAttribute(posAttr, i * 2 + 1);

    closestPointOnSegment3D(localPoint, aLocal, bLocal, _closest);
    const distSq = localPoint.distanceToSquared(_closest);

    if (distSq < bestDistSq) {
      bestDistSq = distSq;
      bestIndex = i;
      bestClosestLocal.copy(_closest);
    }
  }

  if (bestIndex !== -1 && bestDistSq <= thresholdSq) {
    const a = new THREE.Vector3().fromBufferAttribute(posAttr, bestIndex * 2).applyMatrix4(matrix);
    const b = new THREE.Vector3().fromBufferAttribute(posAttr, bestIndex * 2 + 1).applyMatrix4(matrix);
    const midpoint = a.clone().add(b).multiplyScalar(0.5);
    const snapPoint = bestClosestLocal.clone().applyMatrix4(matrix);
    const lengthMM = a.distanceTo(b) * 1000;
    return { midpoint, snapPoint, lengthMM, a, b };
  }
  return null;
}

export function detectCircularEdgeAtPoint3D(
  mesh: THREE.Mesh,
  worldPoint: THREE.Vector3,
  radiusMeters: number = 0.08
): { center: THREE.Vector3; diameterMM: number } | null {
  let edgeLines: THREE.LineSegments | null = null;
  mesh.children.forEach(c => {
    if (c.userData.__edgeLine && c instanceof THREE.LineSegments) edgeLines = c;
  });

  let geo: THREE.BufferGeometry;
  if (edgeLines) {
    geo = edgeLines.geometry;
  } else {
    if (!mesh.userData.__cachedEdgesGeometry) {
      mesh.userData.__cachedEdgesGeometry = new THREE.EdgesGeometry(mesh.geometry, 15);
    }
    geo = mesh.userData.__cachedEdgesGeometry;
  }
  const posAttr = geo.attributes.position;
  if (!posAttr) return null;

  const matrix = edgeLines ? edgeLines.matrixWorld : mesh.matrixWorld;
  const invMatrix = new THREE.Matrix4().copy(matrix).invert();
  const localPoint = worldPoint.clone().applyMatrix4(invMatrix);

  const verts: THREE.Vector3[] = [];
  const seen = new Set<string>();
  const tmp = new THREE.Vector3();
  const radiusSq = radiusMeters * radiusMeters;
  const count = posAttr.count;

  for (let i = 0; i < count; i++) {
    tmp.fromBufferAttribute(posAttr, i);
    const dx = tmp.x - localPoint.x;
    const dy = tmp.y - localPoint.y;
    const dz = tmp.z - localPoint.z;
    const distSq = dx * dx + dy * dy + dz * dz;

    if (distSq > radiusSq) continue;
    const key = `${tmp.x.toFixed(5)},${tmp.y.toFixed(5)},${tmp.z.toFixed(5)}`;
    if (seen.has(key)) continue;
    seen.add(key);
    verts.push(tmp.clone().applyMatrix4(matrix));
  }

  if (verts.length < 6) return null;

  const centroid = new THREE.Vector3();
  verts.forEach(v => centroid.add(v));
  centroid.divideScalar(verts.length);

  let xx = 0, xy = 0, xz = 0, yy = 0, yz = 0, zz = 0;
  for (const v of verts) {
    const dx = v.x - centroid.x, dy = v.y - centroid.y, dz = v.z - centroid.z;
    xx += dx * dx; xy += dx * dy; xz += dx * dz;
    yy += dy * dy; yz += dy * dz; zz += dz * dz;
  }
  const axisA = new THREE.Vector3(xx, xy, xz).normalize();
  const axisB = new THREE.Vector3(xy, yy, yz).normalize();
  const normal = new THREE.Vector3().crossVectors(axisA, axisB);
  if (normal.lengthSq() < 1e-8) return null;
  normal.normalize();

  const basisU = new THREE.Vector3();
  if (Math.abs(normal.x) < 0.9) basisU.crossVectors(normal, new THREE.Vector3(1, 0, 0)).normalize();
  else basisU.crossVectors(normal, new THREE.Vector3(0, 1, 0)).normalize();
  const basisV = new THREE.Vector3().crossVectors(normal, basisU).normalize();

  const points2D = verts.map(v => {
    const rel = v.clone().sub(centroid);
    return { u: rel.dot(basisU), v: rel.dot(basisV) };
  });
  const fit = circleFitKasa(points2D);
  if (!fit) return null;

  const diameterMM = fit.radius * 2 * 1000;
  if (diameterMM < 2 || diameterMM > 500) return null;

  const center3D = centroid.clone()
    .add(basisU.clone().multiplyScalar(fit.cx))
    .add(basisV.clone().multiplyScalar(fit.cy));

  return { center: center3D, diameterMM };
}

export function resolveSnap3D(
  mesh: THREE.Mesh,
  hitPoint: THREE.Vector3,
  vertexThresholdMeters: number = 0.07,
  edgeThresholdMeters: number = 0.09
): { point: THREE.Vector3; type: 'vertex' | 'edge' | 'surface' } {
  const v = findNearestVertex3D(mesh, hitPoint, vertexThresholdMeters);
  if (v) return { point: v, type: 'vertex' };
  const e = findNearestEdgeSegment3D(mesh, hitPoint, edgeThresholdMeters);
  if (e) return { point: e.snapPoint, type: 'edge' };
  return { point: hitPoint.clone(), type: 'surface' };
}
