import { Box, Loader2 } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';

import { isDebugMode } from '../../config/debugMode';
import { readMatch3DGeneratedModelBytes } from '../../services/match3dGeneratedModelCache';

type ThreeModule = typeof import('three');
type GltfPayload = import('three/examples/jsm/loaders/GLTFLoader.js').GLTF;

type PreviewStatus = 'empty' | 'loading' | 'ready' | 'fallback';

type Match3DModelPreviewProps = {
  modelSrc?: string | null;
  className?: string;
};

function hasWebGLSupport() {
  try {
    const canvas = document.createElement('canvas');
    return Boolean(canvas.getContext('webgl2') ?? canvas.getContext('webgl'));
  } catch {
    return false;
  }
}

function disposeThreeObject(object: import('three').Object3D) {
  object.traverse((child) => {
    const mesh = child as import('three').Mesh;
    mesh.geometry?.dispose();
    const material = mesh.material;
    if (Array.isArray(material)) {
      material.forEach((item) => item.dispose());
    } else {
      material?.dispose();
    }
  });
}

function applyCanvasLayout(canvas: HTMLCanvasElement) {
  canvas.style.display = 'block';
  canvas.style.height = '100%';
  canvas.style.inset = '0';
  canvas.style.position = 'absolute';
  canvas.style.width = '100%';
}

function centerAndScaleModel(three: ThreeModule, model: import('three').Object3D) {
  const bounds = new three.Box3().setFromObject(model);
  const size = bounds.getSize(new three.Vector3());
  const maxDimension = Math.max(size.x, size.y, size.z, 0.001);
  const scale = 1.45 / maxDimension;
  model.scale.setScalar(scale);

  const centeredBounds = new three.Box3().setFromObject(model);
  const center = centeredBounds.getCenter(new three.Vector3());
  model.position.sub(center);
}

function shouldLogMatch3DModelPreviewDiagnostics() {
  return isDebugMode() && import.meta.env.MODE !== 'test';
}

function warnMatch3DModelPreviewLoadFailure(source: string, error: unknown) {
  if (!shouldLogMatch3DModelPreviewDiagnostics()) {
    return;
  }
  const message =
    error instanceof Error ? error.message : String(error || 'unknown error');
  console.warn('[match3d] model preview load failed', {
    source,
    message,
  });
}

export function Match3DModelPreview({
  modelSrc,
  className = '',
}: Match3DModelPreviewProps) {
  const containerRef = useRef<HTMLDivElement | null>(null);
  const canvasHostRef = useRef<HTMLDivElement | null>(null);
  const runtimeRef = useRef<{
    animationId: number | null;
    cleanup: (() => void) | null;
    renderer: import('three').WebGLRenderer;
  } | null>(null);
  const [status, setStatus] = useState<PreviewStatus>(
    modelSrc ? 'loading' : 'empty',
  );

  useEffect(() => {
    const container = containerRef.current;
    const canvasHost = canvasHostRef.current;
    if (!container || !canvasHost) {
      return undefined;
    }

    const source = modelSrc?.trim() ?? '';
    if (!source) {
      setStatus('empty');
      runtimeRef.current?.cleanup?.();
      runtimeRef.current = null;
      canvasHost.replaceChildren();
      return undefined;
    }

    let cancelled = false;
    const teardown = () => {
      const runtime = runtimeRef.current;
      if (runtime?.animationId != null) {
        window.cancelAnimationFrame(runtime.animationId);
      }
      runtime?.cleanup?.();
      runtime?.renderer.dispose();
      runtime?.renderer.domElement.remove();
      runtimeRef.current = null;
      canvasHost.replaceChildren();
    };

    const setup = async () => {
      if (!hasWebGLSupport()) {
        setStatus('fallback');
        return;
      }

      setStatus('loading');
      try {
        const [three, loaderModule, bytes] = await Promise.all([
          import('three'),
          import('three/examples/jsm/loaders/GLTFLoader.js'),
          readMatch3DGeneratedModelBytes(source, { expireSeconds: 600 }),
        ]);
        if (cancelled || !containerRef.current) {
          return;
        }

        if (bytes.byteLength === 0) {
          throw new Error('empty model');
        }

        const renderer = new three.WebGLRenderer({
          alpha: true,
          antialias: true,
        });
        renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 1.8));
        renderer.outputColorSpace = three.SRGBColorSpace;
        applyCanvasLayout(renderer.domElement);
        canvasHost.appendChild(renderer.domElement);

        const scene = new three.Scene();
        scene.background = null;

        const camera = new three.PerspectiveCamera(35, 1, 0.1, 100);
        camera.position.set(0.06, 0.92, 2.8);
        camera.lookAt(0, 0.1, 0);

        scene.add(new three.AmbientLight(0xffffff, 1.55));

        const keyLight = new three.DirectionalLight(0xffffff, 2.8);
        keyLight.position.set(-3.6, 4.8, 3.5);
        scene.add(keyLight);

        const fillLight = new three.DirectionalLight(0xfef3c7, 0.62);
        fillLight.position.set(2.5, 1.8, -3.2);
        scene.add(fillLight);

        const rimLight = new three.DirectionalLight(0xffffff, 0.8);
        rimLight.position.set(1.8, 3.6, -4.4);
        scene.add(rimLight);

        const modelRoot = new three.Group();
        modelRoot.rotation.set(0.2, 0.45, 0.02);
        scene.add(modelRoot);

        const loader = new loaderModule.GLTFLoader();
        const gltf = (await loader.parseAsync(bytes, '')) as GltfPayload;
        if (cancelled) {
          const cancelledModel = gltf.scene ?? gltf.scenes[0];
          if (cancelledModel) {
            disposeThreeObject(cancelledModel);
          }
          return;
        }

        const model = gltf.scene ?? gltf.scenes[0];
        if (!model) {
          throw new Error('missing model scene');
        }

        modelRoot.add(model);
        centerAndScaleModel(three, model);

        const resize = () => {
          const rect = container.getBoundingClientRect();
          const width = Math.max(1, rect.width);
          const height = Math.max(1, rect.height);
          renderer.setSize(width, height, false);
          camera.aspect = width / height;
          camera.updateProjectionMatrix();
          renderer.render(scene, camera);
        };

        const resizeObserver = window.ResizeObserver
          ? new window.ResizeObserver(resize)
          : null;
        resizeObserver?.observe(container);
        resize();

        const pointerState = {
          dragging: false,
          lastX: 0,
          lastY: 0,
        };

        const handlePointerDown = (event: PointerEvent) => {
          if (event.button !== 0) {
            return;
          }
          pointerState.dragging = true;
          pointerState.lastX = event.clientX;
          pointerState.lastY = event.clientY;
          if (typeof container.setPointerCapture === 'function') {
            container.setPointerCapture(event.pointerId);
          }
        };
        const handlePointerMove = (event: PointerEvent) => {
          if (!pointerState.dragging) {
            return;
          }
          const deltaX = event.clientX - pointerState.lastX;
          const deltaY = event.clientY - pointerState.lastY;
          pointerState.lastX = event.clientX;
          pointerState.lastY = event.clientY;
          modelRoot.rotation.y += deltaX * 0.01;
          modelRoot.rotation.x = Math.max(
            -1.15,
            Math.min(1.15, modelRoot.rotation.x + deltaY * 0.01),
          );
          renderer.render(scene, camera);
        };
        const handlePointerEnd = (event: PointerEvent) => {
          pointerState.dragging = false;
          if (
            typeof container.hasPointerCapture === 'function' &&
            container.hasPointerCapture(event.pointerId)
          ) {
            container.releasePointerCapture(event.pointerId);
          }
        };

        container.addEventListener('pointerdown', handlePointerDown);
        container.addEventListener('pointermove', handlePointerMove);
        container.addEventListener('pointerup', handlePointerEnd);
        container.addEventListener('pointercancel', handlePointerEnd);

        const animate = () => {
          renderer.render(scene, camera);
          if (runtimeRef.current) {
            runtimeRef.current.animationId = window.requestAnimationFrame(animate);
          }
        };

        runtimeRef.current = {
          animationId: window.requestAnimationFrame(animate),
          cleanup: () => {
            resizeObserver?.disconnect();
            container.removeEventListener('pointerdown', handlePointerDown);
            container.removeEventListener('pointermove', handlePointerMove);
            container.removeEventListener('pointerup', handlePointerEnd);
            container.removeEventListener('pointercancel', handlePointerEnd);
            disposeThreeObject(modelRoot);
          },
          renderer,
        };

        setStatus('ready');
      } catch (caughtError) {
        if (!cancelled) {
          warnMatch3DModelPreviewLoadFailure(source, caughtError);
          setStatus('fallback');
        }
      }
    };

    void setup();

    return () => {
      cancelled = true;
      teardown();
    };
  }, [modelSrc]);

  return (
    <div
      ref={containerRef}
      aria-label="3D 模型预览"
      className={`relative overflow-hidden rounded-[1.15rem] border border-[var(--platform-subpanel-border)] bg-white/78 ${className}`}
      data-testid="match3d-model-preview"
    >
      <div ref={canvasHostRef} aria-hidden="true" className="absolute inset-0" />
      {status !== 'ready' ? (
        <div className="absolute inset-0 grid place-items-center text-[var(--platform-text-soft)]">
          {status === 'loading' ? (
            <Loader2 className="h-8 w-8 animate-spin" />
          ) : (
            <Box className="h-8 w-8" />
          )}
        </div>
      ) : null}
      <div className="pointer-events-none absolute inset-0 z-10" />
    </div>
  );
}
