import { useRef, useState, useMemo, useEffect, Suspense, Component } from 'react';
import { useThree, useLoader } from '@react-three/fiber';
import * as THREE from 'three';
import { TextureLoader } from 'three';

// Preview placement constants. Final surface projection requires the Geometry Engine.
const FRONT_Z = 2.25;
const BASE = 2.4;
const FALLBACK_TEX = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYPhfDwAChwGA60e6kAAAAABJRU5ErkJggg==';
const SNAP = 0.08;

/**
 * Per-layer error boundary: a single broken texture URL only hides that layer,
 * it does not crash the Canvas or the route.
 */
class LayerErrorBoundary extends Component {
  constructor(p) { super(p); this.state = { failed: false }; }
  static getDerivedStateFromError() { return { failed: true }; }
  componentDidCatch(e) {
    if (import.meta.env?.DEV) console.error('[Overlay] layer texture failed:', e);
  }
  render() { return this.state.failed ? null : this.props.children; }
}

function ArtworkLayerView({ layer, asset, selected, onSelect }) {
  const url = asset?.processedFile || asset?.originalFile || FALLBACK_TEX;
  const tex = useLoader(TextureLoader, url);
  useMemo(() => {
    tex.colorSpace = THREE.SRGBColorSpace;
    tex.transparent = true;
    tex.minFilter = THREE.LinearFilter;
    tex.magFilter = THREE.LinearFilter;
  }, [tex]);
  // Memoise the boundary edges geometry once and dispose on unmount — avoids
  // allocating a new PlaneGeometry every render (WebGL memory leak on repeat
  // Studio navigation). BASE×BASE because the parent group carries the scale.
  const edges = useMemo(() => {
    const pg = new THREE.PlaneGeometry(BASE, BASE);
    const eg = new THREE.EdgesGeometry(pg);
    pg.dispose();
    return eg;
  }, []);
  useEffect(() => () => edges.dispose(), [edges]);
  if (!layer.visible) return null;
  const sx = Math.abs(layer.scaleX || 1), sy = Math.abs(layer.scaleY || 1);
  const rot = THREE.MathUtils.degToRad(layer.rotation || 0);
  return (
    <group position={[layer.positionX || 0, layer.positionY || 0, FRONT_Z]} rotation={[0, 0, rot]} scale={[sx, sy, 1]}>
      <mesh
        onClick={(e) => { e.stopPropagation(); onSelect?.(layer); }}
        onPointerDown={(e) => { if (selected?.id === layer.id) e.stopPropagation(); }}
      >
        <planeGeometry args={[BASE, BASE]} />
        <meshBasicMaterial map={tex} transparent opacity={layer.opacity ?? 1} side={THREE.DoubleSide} depthWrite={false} />
      </mesh>
      {/* boundary outline when selected — geometry is unit-sized; the group scale applies */}
      {selected?.id === layer.id && !layer.locked && (
        <lineSegments geometry={edges}>
          <lineBasicMaterial color="#22d3ee" transparent opacity={0.9} />
        </lineSegments>
      )}
    </group>
  );
}

// Direct-manipulation handles for the selected layer. Deltas are computed from
// a world point captured at drag start; all math is null-guarded so a pointer
// that misses the plane cannot throw.
function TransformHandles({ layer, lockAspect, onLive, onCommit, decorationZones }) {
  const { camera, raycaster, pointer } = useThree();
  const drag = useRef(null);
  const [, force] = useState(0);

  const sx = Math.abs(layer.scaleX || 1), sy = Math.abs(layer.scaleY || 1);
  const rot = THREE.MathUtils.degToRad(layer.rotation || 0);
  const cx = layer.positionX || 0, cy = layer.positionY || 0;

  const corners = useMemo(() => [
    ['c-tl', -BASE * sx / 2, BASE * sy / 2], ['c-tr', BASE * sx / 2, BASE * sy / 2],
    ['c-bl', -BASE * sx / 2, -BASE * sy / 2], ['c-br', BASE * sx / 2, -BASE * sy / 2],
  ], [sx, sy]);
  const edges = useMemo(() => [
    ['e-t', 0, BASE * sy / 2], ['e-b', 0, -BASE * sy / 2],
    ['e-l', -BASE * sx / 2, 0], ['e-r', BASE * sx / 2, 0],
  ], [sx, sy]);

  const worldPoint = () => {
    raycaster.setFromCamera(pointer, camera);
    const plane = new THREE.Plane(new THREE.Vector3(0, 0, 1), -FRONT_Z);
    const v = new THREE.Vector3();
    return raycaster.ray.intersectPlane(plane, v); // may be null
  };
  const localPoint = (w) => {
    if (!w) return { x: 0, y: 0 };
    const dx = w.x - cx, dy = w.y - cy;
    const cs = Math.cos(-rot), sn = Math.sin(-rot);
    return { x: dx * cs - dy * sn, y: dx * sn + dy * cs };
  };

  const start = (mode, e) => {
    e.stopPropagation();
    const w = worldPoint();
    if (!w) return; // pointer did not intersect the plane — do not start a drag
    e.target?.setPointerCapture?.(e.pointerId);
    drag.current = {
      mode,
      startWorld: { x: w.x, y: w.y },
      startLayer: { positionX: layer.positionX || 0, positionY: layer.positionY || 0, scaleX: layer.scaleX ?? 1, scaleY: layer.scaleY ?? 1, rotation: layer.rotation || 0 },
      startLocal: localPoint(w),
    };
  };

  const move = (e) => {
    if (!drag.current) return;
    e.stopPropagation();
    const w = worldPoint();
    if (!w) return;
    const d = drag.current, dl = d.startLayer;
    const dx = w.x - d.startWorld.x, dy = w.y - d.startWorld.y;
    let patch = {};
    if (d.mode === 'move') {
      let nx = dl.positionX + dx, ny = dl.positionY + dy;
      if (Math.abs(nx) < SNAP) nx = 0;          // snap to centre
      if (Math.abs(ny) < SNAP) ny = 0;
      for (const z of Array.isArray(decorationZones) ? decorationZones : []) {
        const zx = z?.position?.x, zy = z?.position?.y;
        if (typeof zx === 'number' && Math.abs(nx - zx) < SNAP) nx = zx;
        if (typeof zy === 'number' && Math.abs(ny - zy) < SNAP) ny = zy;
      }
      patch = { positionX: Number(nx.toFixed(3)), positionY: Number(ny.toFixed(3)) };
    } else if (d.mode === 'rotate') {
      const a0 = Math.atan2(d.startLocal.y, d.startLocal.x);
      const lp = localPoint(w);
      const a1 = Math.atan2(lp.y, lp.x);
      patch = { rotation: Number((dl.rotation + THREE.MathUtils.radToDeg(a1 - a0)).toFixed(1)) };
    } else {
      const lp = localPoint(w);
      const ratioX = lp.x / (d.startLocal.x || 0.001);
      const ratioY = lp.y / (d.startLocal.y || 0.001);
      if (d.mode === 'scale') {
        const r = Math.max(0.1, Math.abs(ratioX) * 0.5 + Math.abs(ratioY) * 0.5);
        const ns = Math.max(0.1, Math.min(4, (Math.abs(dl.scaleX) || 1) * r)) * Math.sign(dl.scaleX || 1);
        patch = lockAspect ? { scaleX: Number(ns.toFixed(3)), scaleY: Number((Math.abs(ns) * Math.sign(dl.scaleY || 1)).toFixed(3)) } : { scaleX: Number(ns.toFixed(3)) };
      } else if (d.mode === 'scaleY') {
        const ns = Math.max(0.1, Math.min(4, (Math.abs(dl.scaleY) || 1) * Math.abs(ratioY))) * Math.sign(dl.scaleY || 1);
        patch = { scaleY: Number(ns.toFixed(3)) };
      } else if (d.mode === 'scaleX') {
        const ns = Math.max(0.1, Math.min(4, (Math.abs(dl.scaleX) || 1) * Math.abs(ratioX))) * Math.sign(dl.scaleX || 1);
        patch = { scaleX: Number(ns.toFixed(3)) };
      }
    }
    onLive?.(layer, patch);
    force((n) => n + 1);
  };

  const end = (e) => {
    if (!drag.current) return;
    e.stopPropagation();
    drag.current = null;
    onCommit?.(layer);
  };

  if (layer.locked || !layer.visible) return null;

  const handle = (key, x, y, color, mode, extra) => (
    <mesh key={key} position={[x, y, 0.01]}
      onPointerDown={(e) => start(mode, e)} onPointerMove={move} onPointerUp={end} onPointerCancel={end}>
      <boxGeometry args={extra || [0.16, 0.16, 0.02]} />
      <meshBasicMaterial color={color} depthTest={false} transparent />
    </mesh>
  );

  return (
    <group position={[cx, cy, FRONT_Z + 0.02]} rotation={[0, 0, rot]} renderOrder={999}>
      {/* move body (invisible catcher over the plane) */}
      <mesh onPointerDown={(e) => start('move', e)} onPointerMove={move} onPointerUp={end} onPointerCancel={end}>
        <planeGeometry args={[BASE * sx, BASE * sy]} />
        <meshBasicMaterial transparent opacity={0} depthWrite={false} colorWrite={false} />
      </mesh>
      {corners.map(([k, x, y]) => handle(k, x, y, '#22d3ee', 'scale'))}
      {edges.map(([k, x, y]) => handle(k, x, y, '#67e8f9', k.endsWith('l') || k.endsWith('r') ? 'scaleX' : 'scaleY', [0.14, 0.06, 0.02]))}
      {/* rotation handle */}
      <mesh position={[0, BASE * sy / 2 + 0.35, 0.01]} onPointerDown={(e) => start('rotate', e)} onPointerMove={move} onPointerUp={end} onPointerCancel={end}>
        <sphereGeometry args={[0.12, 16, 16]} />
        <meshBasicMaterial color="#a78bfa" depthTest={false} transparent />
      </mesh>
      <mesh position={[0, BASE * sy / 2, 0.01]}>
        <boxGeometry args={[0.03, 0.35, 0.02]} />
        <meshBasicMaterial color="#a78bfa" transparent opacity={0.6} />
      </mesh>
      {/* centre point */}
      <mesh position={[0, 0, 0.01]}>
        <ringGeometry args={[0.05, 0.08, 24]} />
        <meshBasicMaterial color="#fbbf24" depthTest={false} transparent />
      </mesh>
    </group>
  );
}

export default function ArtworkOverlay({ layers, assets, selected, lockAspect, decorationZones, onLive, onCommit, onSelect }) {
  const assetFor = (l) => (Array.isArray(assets) ? assets.find((a) => a.id === l?.artworkAssetId) : undefined);
  const visible = Array.isArray(layers) ? layers.filter((l) => l.visible) : [];
  return (
    <Suspense fallback={null}>
      <group>
        {visible.map((l) => {
          const a = assetFor(l);
          const layerUrl = a?.processedFile || a?.originalFile || '';
          return (
            <LayerErrorBoundary key={`${l.id}:${layerUrl}`}>
              <ArtworkLayerView layer={l} asset={a} selected={selected} onSelect={onSelect} />
            </LayerErrorBoundary>
          );
        })}
        {selected && assetFor(selected) && (
          <TransformHandles
            layer={selected}
            lockAspect={lockAspect}
            decorationZones={decorationZones}
            onLive={onLive}
            onCommit={onCommit}
          />
        )}
      </group>
    </Suspense>
  );
}