import React, { Suspense, useEffect, useMemo, useRef, useState, useCallback } from "react";
import { Canvas, useThree, useFrame } from "@react-three/fiber";
import { OrbitControls, Html, useGLTF, Bounds, useFBX, useAnimations, useProgress } from "@react-three/drei";
import * as THREE from "three";

/**
 * --- 1. 模型加载模块 (Model Loading) ---
 * 负责不同格式模型的加载 (GLTF, FBX) 以及动画播放
 */

// 导出 useFetchLoader 供外部使用
export function useFetchLoader(url) {
  const [state, setState] = useState({
    progress: 0,
    loaded: 0,
    total: 0,
    blobUrl: null,
    error: null,
    loading: false,
  });

  useEffect(() => {
    if (!url) return;

    const controller = new AbortController();
    let objectUrl = null;

    const load = async () => {
      setState((s) => ({ ...s, loading: true, progress: 0, loaded: 0, total: 0, error: null }));

      try {
        const response = await fetch(url, { signal: controller.signal });
        if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);

        const contentLength = response.headers.get("content-length");
        const total = contentLength ? parseInt(contentLength, 10) : 0;

        const reader = response.body.getReader();
        let receivedLength = 0;
        const chunks = [];

        while (true) {
          const { done, value } = await reader.read();
          if (done) break;

          chunks.push(value);
          receivedLength += value.length;

          setState((prev) => ({
            ...prev,
            loaded: receivedLength,
            total: total,
            progress: total ? (receivedLength / total) * 100 : 0,
          }));
        }

        const blob = new Blob(chunks);
        objectUrl = URL.createObjectURL(blob);

        setState((prev) => ({
          ...prev,
          blobUrl: objectUrl,
          loading: false,
          progress: 100,
        }));
      } catch (e) {
        if (e.name !== "AbortError") {
          console.error("Fetch Loader error:", e);
          setState((prev) => ({ ...prev, error: e.message, loading: false }));
        }
      }
    };

    load();

    return () => {
      controller.abort();
      if (objectUrl) URL.revokeObjectURL(objectUrl);
    };
  }, [url]);

  return state;
}

function Loader({ customState, color }) {
  // 只负责显示手动下载的进度
  // 如果没有在下载，直接返回 null
  if (!customState || !customState.loading) return null;

  return <LoaderUI progress={customState.progress} loaded={customState.loaded} total={customState.total} status="downloading" color={color} />;
}

// 专门用于 Suspense fallback 的解析状态提示
function ParsingLoader({ color }) {
  return <LoaderUI progress={100} loaded={0} total={0} status="parsing" color={color} />;
}

function LoaderUI({ progress, loaded, total, status, color }) {
  // 判断是否无法计算总大小
  const isIndeterminate = status === "downloading" && total === 0 && loaded > 0;

  return (
    <Html fullscreen style={{ zIndex: 9999, pointerEvents: "none" }}>
      <div
        style={{
          width: "100%",
          height: "100%",
          background: "#ffffff", // 统一使用白色背景，防止阶段切换时的闪烁
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
        }}
      >
        <div
          style={{
            width: 600,
            height: 10,
            background: "#fff", // 未加载部分是白色
            borderRadius: 5, // 稍微圆角一点更好看，或者如果想要完全直角可以设为0
            overflow: "hidden",
            position: "relative",
            boxShadow: "0 2px 8px rgba(0,0,0,0.2)",
          }}
        >
          <div
            style={{
              width: isIndeterminate ? "100%" : `${progress}%`,
              height: "100%",
              background: color || "#4C7F7A", // 已加载进度颜色
              transition: isIndeterminate ? "none" : "width 0.2s ease-out",
              position: "absolute",
              left: 0,
              top: 0,
              backgroundImage: isIndeterminate ? "linear-gradient(45deg,rgba(255,255,255,.3) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.3) 50%,rgba(255,255,255,.3) 75%,transparent 75%,transparent)" : "none",
              backgroundSize: "20px 20px",
              animation: isIndeterminate ? "progress-bar-stripes 1s linear infinite" : "none",
            }}
          />
        </div>
      </div>
      <style>{`
        @keyframes progress-bar-stripes {
          from { background-position: 20px 0; }
          to { background-position: 0 0; }
        }
      `}</style>
    </Html>
  );
}

export function OverlayLoader({ fetchState, isModelReady, color, active }) {
  // active: 是否应该处于激活状态（只要有 src 就应该激活）
  
  // 计算当前状态
  const isDownloading = fetchState && fetchState.loading;
  // 只要有 blobUrl 但场景没好，就是在解析或自适应
  const isProcessing = fetchState && fetchState.blobUrl && !isModelReady;
  
  // 只要 active 为 true，且模型未就绪，就应该显示
  // 如果 active 为 false (无 src)，则隐藏
  // 如果模型已就绪 (isModelReady)，则隐藏
  const shouldShow = active && !isModelReady;

  // 进度条逻辑
  let progress = 0;
  if (isDownloading) progress = fetchState.progress;
  else if (isProcessing) progress = 100;
  else if (shouldShow) progress = 0; // 初始状态

  return (
    <div
      style={{
        position: "fixed",
        top: 0,
        left: 0,
        width: "100%",
        height: "100%",
        background: "#ffffff",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        opacity: shouldShow ? 1 : 0,
        // 关键修改：显示时瞬间显示(无transition)，隐藏时才淡出
        transition: shouldShow ? "none" : "opacity 0.8s ease-out",
        pointerEvents: shouldShow ? "auto" : "none",
        zIndex: 9999,
      }}
    >
      <div
        style={{
          width: 600,
          height: 10,
          background: "#fff",
          borderRadius: 5,
          overflow: "hidden",
          position: "relative",
          boxShadow: "0 2px 8px rgba(0,0,0,0.2)",
        }}
      >
        <div
          style={{
            width: "100%",
            transform: `scaleX(${progress / 100})`,
            transformOrigin: "left",
            height: "100%",
            background: color || "#4C7F7A",
            transition: "transform 0.2s linear",
          }}
        />
      </div>
    </div>
  );
}

// GLTF 模型组件
function GLTFModel({ url, ...props }) {
  const { scene, animations } = useGLTF(url, true);
  const ref = useRef();
  const { actions, names } = useAnimations(animations, ref);

  // 自动播放第一个动画
  useEffect(() => {
    if (actions && names.length > 0) {
      actions[names[0]].reset().fadeIn(0.5).play();
    }
  }, [actions, names]);

  return <primitive ref={ref} object={scene} {...props} />;
}

// FBX 模型组件
function FBXModel({ url, ...props }) {
  const obj = useFBX(url);
  const ref = useRef();
  const { actions, names } = useAnimations(obj.animations || [], ref);

  // 自动播放第一个动画
  useEffect(() => {
    if (actions && names.length > 0) {
      actions[names[0]].reset().fadeIn(0.5).play();
    }
  }, [actions, names]);

  return <primitive ref={ref} object={obj} {...props} />;
}

// 预留 USDZ 支持 (iOS ARQuickLook 格式)
function USDZModel({ url }) {
  // 注意：useGLTF 默认不支持 .usdz，需配合特定 loader 或其他库
  // 这里仅作占位，方便后续扩展
  return null;
}

// 统一模型加载器
// 根据 URL 后缀分发到对应的加载组件
function ModelLoader({ url, fileType }) {
  const kind = useMemo(() => {
    // 如果显式传递了 fileType，则直接使用
    if (fileType) return fileType;

    // 否则尝试从 URL 推断 (Blob URL 通常无法推断，所以 fileType 很重要)
    const u = (url || "").toLowerCase().split("?")[0].split("#")[0];
    if (u.endsWith(".fbx")) return "fbx";
    if (u.endsWith(".usdz")) return "usdz";
    return "gltf";
  }, [url, fileType]);

  switch (kind) {
    case "fbx":
      return <FBXModel url={url} />;
    case "usdz":
      return <USDZModel url={url} />;
    default:
      return <GLTFModel url={url} />;
  }
}

/**
 * --- 2. 交互逻辑模块 (Interaction Logic) ---
 * 包含：自动适应检测、相机控制、交互锁定
 */

// FitWatcher: 负责检测 Bounds 组件的自适应动画是否完成
// 原理：监听相机位置和目标点的变化，当连续多帧保持静止时，认为自适应完成
function FitWatcher({ controlsRef, onFitReady, zoomMin }) {
  const { camera } = useThree();
  // 记录上一帧的状态，用于对比差异
  const lastState = useRef({ pos: new THREE.Vector3(), target: new THREE.Vector3() });
  const stableFrames = useRef(0);
  const isFitted = useRef(false);
  const startTime = useRef(Date.now()); // 记录组件挂载时间

  // 当 URL 变化时重置检测状态，重新开始监听
  useEffect(() => {
    isFitted.current = false;
    stableFrames.current = 0;
    startTime.current = Date.now();
  }, [onFitReady]);

  // useFrame 会在每一帧渲染时执行
  useFrame(() => {
    if (!controlsRef.current || isFitted.current) return;

    // 强制等待至少 1000ms，确保 Bounds 有足够时间启动动画，且避免缓存加载时 Loader 闪现
    // 这段时间内，Loader 会一直遮挡屏幕
    if (Date.now() - startTime.current < 1000) return;

    const controls = controlsRef.current;
    const currentPos = camera.position;
    const currentTarget = controls.target;

    const distDiff = currentPos.distanceTo(lastState.current.pos);
    const targetDiff = currentTarget.distanceTo(lastState.current.target);

    // 只有当相机几乎静止时（位移 < 0.001）才开始计数
    if (distDiff < 0.001 && targetDiff < 0.001) {
      stableFrames.current++;
    } else {
      stableFrames.current = 0;
    }

    lastState.current.pos.copy(currentPos);
    lastState.current.target.copy(currentTarget);

    // 连续 20 帧稳定（约 330ms @ 60fps）认为 fit 动画彻底结束
    if (stableFrames.current > 20) {
      isFitted.current = true;
      const distance = currentPos.distanceTo(currentTarget);
      
      // 核心修复：如果设置了 zoomMin (例如 2)，意味着用户希望初始视图就放大2倍
      // Bounds 组件默认只会 fit 到正好填满屏幕 (zoom=1)
      // 所以我们需要在这里手动应用初始缩放，防止后续 CameraController 里的 useEffect 突然跳变
      
      const initialZoom = Math.max(1, Number(zoomMin) || 1);
      
      if (initialZoom > 1) {
          // 计算缩放后的目标位置：保持方向不变，距离缩短
          const direction = new THREE.Vector3().subVectors(currentPos, currentTarget).normalize();
          const newDistance = distance / initialZoom;
          const newPos = currentTarget.clone().add(direction.multiplyScalar(newDistance));
          
          // 直接应用新位置，避免动画跳变
          camera.position.copy(newPos);
          // 更新 distance 为缩放后的距离，作为后续的基础距离
          console.log(`Initial Fit adjusted by zoomMin=${initialZoom}: ${distance} -> ${newDistance}`);
          
          onFitReady({
            position: newPos,
            target: currentTarget.clone(),
            distance: newDistance, // 这里的 distance 已经是被 zoomMin 除过的了
            baseDistance: distance // 记录原始的 "1倍" 距离作为真正的 baseDistance
          });
      } else {
          console.log("Fit stable, distance:", distance);
          onFitReady({
            position: currentPos.clone(),
            target: currentTarget.clone(),
            distance: distance,
            baseDistance: distance
          });
      }
    }
  });

  return null;
}

// CameraController: 封装 OrbitControls 并处理缩放限制
function CameraController({ controlsRef, zoomMin, zoomMax, baseDistance, autoRotateSpeed, enabled = true, onStart, onEnd }) {
  // 动态计算 maxDistance 和 minDistance
  useEffect(() => {
    const controls = controlsRef.current;
    if (!controls || baseDistance == null) return;

    // zoomMin (default 1) -> 最小放大倍数 -> 意味着相机拉得最远 -> 对应 maxDistance
    // zoomMax (default 4) -> 最大放大倍数 -> 意味着相机拉得最近 -> 对应 minDistance
    // 公式：实际距离 = 基础距离(baseDistance) / 缩放倍率(zoom)

    // 防止除以 0
    const minZ = Math.max(0.001, zoomMin || 1);
    const maxZ = Math.max(minZ, zoomMax || 4);

    controls.maxDistance = baseDistance / minZ;
    controls.minDistance = baseDistance / maxZ;
  }, [baseDistance, zoomMin, zoomMax, controlsRef]);

  return (
    <OrbitControls
      ref={controlsRef}
      enabled={enabled} // 在 fit 完成前禁用交互，防止用户打断自动飞入动画
      enableDamping
      dampingFactor={0.08}
      rotateSpeed={0.8}
      zoomSpeed={1.0}
      enableZoom
      enablePan
      panSpeed={0.8}
      screenSpacePanning
      touches={{
        ONE: THREE.TOUCH.ROTATE,
        TWO: THREE.TOUCH.DOLLY_PAN,
      }}
      makeDefault
      autoRotate={!!autoRotateSpeed}
      autoRotateSpeed={autoRotateSpeed}
      onStart={onStart}
      onEnd={onEnd}
    />
  );
}

/**
 * --- 3. 场景容器 (Scene Container) ---
 * 组装模型、灯光、交互，并管理全局状态
 */

function LoadingOverlay({ visible, color }) {
  // 修改：即使 visible 为 false，也不返回 null，而是通过 opacity 控制显示/隐藏
  // 这样 CSS transition 才能生效，实现淡出效果
  return (
    <Html fullscreen style={{ pointerEvents: "none", zIndex: 9998 }}>
      <div
        style={{
          width: "100%",
          height: "100%",
          background: "#ffffff",
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
          opacity: visible ? 1 : 0, // 通过透明度控制显隐
          transition: "opacity 0.8s ease-out", // 增加淡出时长，让过程更平滑
          pointerEvents: visible ? "auto" : "none", // 隐藏后不阻挡点击
        }}
      >
        <div
          style={{
            width: 600,
            height: 10,
            background: "#fff",
            borderRadius: 5,
            overflow: "hidden",
            position: "relative",
            boxShadow: "0 2px 8px rgba(0,0,0,0.2)",
          }}
        >
          <div
            style={{
              width: "100%",
              height: "100%",
              background: color || "#4C7F7A",
            }}
          />
        </div>
      </div>
    </Html>
  );
}

function SceneContent({ url, zoomMin, zoomMax, fileType, onReady }) {
  const controlsRef = useRef();
  const { camera, gl } = useThree();

  // baseDistance: 模型自适应后的基础观察距离，用于计算缩放限制
  const [baseDistance, setBaseDistance] = useState(null);

  // 自动旋转配置
  const [autoRotateConfig, setAutoRotateConfig] = useState({
    active: false, // 是否开启了自动旋转模式（总开关）
    speed: 1.0, // 旋转速度
    delay: 5000, // 无操作后的恢复延迟 (ms)
    isInteracting: false, // 用户是否正在交互
  });

  // isReady: 标记是否完成初始飞入动画，完成后才解锁交互
  // 修改：默认 false，既控制交互也控制场景的可见性（避免跳变被用户看到）
  const [isReady, setIsReady] = useState(false);
  
  // 新增：Fit 状态标记，只有当 FitWatcher 完成调整后才设为 true
  // 我们用它来控制 LoadingOverlay 的消失
  // const [fitAdjusted, setFitAdjusted] = useState(false); // 已被 UnifiedLoader 接管

  // 记录初始 fit 后的完美状态（位置 + 目标点），用于双击复位
  const initialState = useRef(null);
  const timerRef = useRef(null);

  // 当 FitWatcher 告诉我们相机稳定时调用
  const handleFitReady = useCallback((state) => {
    initialState.current = {
      position: state.position,
      target: state.target,
    };
    // 如果 state 中有 baseDistance (原始1倍距离)，则使用它，否则使用当前距离
    // 这样做的目的是：CameraController 需要知道 "1倍缩放" 对应的距离是多少，
    // 以便正确计算 maxDistance (baseDistance / zoomMin)
    setBaseDistance(state.baseDistance || state.distance);
    setIsReady(true); // 解锁交互
    
    // 稍微延迟一点点移除遮罩，确保渲染已经更新到新位置
    if (onReady) {
      setTimeout(() => onReady(), 50);
    }
  }, [onReady]);

  // 平滑复位函数：使用插值动画让相机回到初始状态
  // 支持 onComplete 回调，用于在复位完成后执行操作（如开始自动旋转）
  const resetToInitial = useCallback(
    (onComplete) => {
      if (!controlsRef.current || !initialState.current) return;

      const controls = controlsRef.current;
      const startPos = camera.position.clone();
      const startTarget = controls.target.clone();
      const endPos = initialState.current.position;
      const endTarget = initialState.current.target;

      const duration = 800; // 动画时长 ms
      const startTime = performance.now();

      const animate = (time) => {
        const elapsed = time - startTime;
        const t = Math.min(elapsed / duration, 1);
        const ease = 1 - Math.pow(1 - t, 3); // cubic ease-out 缓动效果

        camera.position.lerpVectors(startPos, endPos, ease);
        controls.target.lerpVectors(startTarget, endTarget, ease);
        controls.update();

        if (t < 1) {
          requestAnimationFrame(animate);
        } else {
          if (typeof onComplete === "function") {
            onComplete();
          }
        }
      };
      requestAnimationFrame(animate);
    },
    [camera],
  );

  // 处理用户交互开始
  const handleInteractionStart = useCallback(() => {
    setAutoRotateConfig((prev) => ({ ...prev, isInteracting: true }));
    if (timerRef.current) {
      clearTimeout(timerRef.current);
      timerRef.current = null;
    }
  }, []);

  // 处理用户交互结束
  const handleInteractionEnd = useCallback(() => {
    setAutoRotateConfig((prev) => ({ ...prev, isInteracting: false }));

    // 如果开启了自动旋转模式，则启动定时器恢复旋转
    if (autoRotateConfig.active) {
      setAutoRotateConfig((prev) => ({ ...prev, isWaiting: true }));

      timerRef.current = setTimeout(() => {
        // 核心修改：等待结束后，先执行复位，复位完成后再恢复旋转
        console.log("无操作超时，开始复位并恢复旋转...");
        resetToInitial(() => {
          // 复位动画完成的回调
          setAutoRotateConfig((prev) => ({ ...prev, isWaiting: false }));
        });
      }, autoRotateConfig.delay);
    }
  }, [autoRotateConfig.active, autoRotateConfig.delay, resetToInitial]);

  // 计算最终传给 Controls 的旋转速度
  // 只有当：总开关开启(active) 且 没有在交互(isInteracting) 且 没有在等待延迟(isWaiting) 时，才有速度
  const effectiveSpeed = autoRotateConfig.active && !autoRotateConfig.isInteracting && !autoRotateConfig.isWaiting ? autoRotateConfig.speed : 0;

  // 通用相机位移动画函数
  const animateCamera = useCallback(
    (targetPos, onComplete) => {
      if (!controlsRef.current) return;
      const controls = controlsRef.current;

      const startPos = camera.position.clone();
      const startTime = performance.now();
      const duration = 300;

      const animate = (time) => {
        const t = Math.min((time - startTime) / duration, 1);
        const ease = 1 - Math.pow(1 - t, 3); // cubic ease-out

        camera.position.lerpVectors(startPos, targetPos, ease);
        controls.update();

        if (t < 1) {
          requestAnimationFrame(animate);
        } else {
          if (typeof onComplete === "function") {
            onComplete();
          }
        }
      };
      requestAnimationFrame(animate);
    },
    [camera],
  );

  // 计算新的相机位置（不改变方向，只改变距离）
  const calculateZoomPos = useCallback(
    (factor, isZoomIn) => {
      if (!controlsRef.current || !baseDistance) return null;
      const controls = controlsRef.current;

      const currentDist = camera.position.distanceTo(controls.target);
      // 放大是除以倍数（距离变小），缩小是乘以倍数（距离变大）
      const targetDist = isZoomIn ? currentDist / factor : currentDist * factor;

      // 检查限制
      if (isZoomIn && targetDist < controls.minDistance) {
        console.warn("已达到最大放大倍数");
        return null;
      }
      if (!isZoomIn && targetDist > controls.maxDistance) {
        console.warn("已达到最小缩小倍数");
        // 允许缩放到 maxDistance，但不超过
      }
      const finalDist = Math.max(controls.minDistance, Math.min(targetDist, controls.maxDistance));

      const direction = new THREE.Vector3().subVectors(camera.position, controls.target).normalize();
      return controls.target.clone().add(direction.multiplyScalar(finalDist));
    },
    [camera, baseDistance],
  );

  // 暴露全局 API 给外部使用 (window.sceneFc)
  useEffect(() => {
    window.sceneFc = {
      resetView: () => {
        // 手动重置时，如果当前正在自动旋转，是否应该关闭自动旋转？
        // 通常手动重置意味着用户想回到初始状态，可能希望停止自动旋转。
        // 这里我们假设手动重置会关闭自动旋转，除非用户再次调用 setAutoRotate
        setAutoRotateConfig((prev) => ({ ...prev, active: false }));
        resetToInitial();
        console.log("已调用重置方法！");
      },
      setAutoRotate: (speed = 1.0, delay = 5000) => {
        const s = Math.max(0.1, Math.min(8, Number(speed) || 1));
        const d = Math.max(0, Number(delay) || 5000);

        console.log(`设置自动旋转: 速度=${s}, 延迟=${d}ms`);

        // 1. 先关闭当前的旋转（如果正在转）
        setAutoRotateConfig((prev) => ({ ...prev, active: false }));

        // 2. 调用重置
        resetToInitial(() => {
          // 3. 重置完成后，开启自动旋转
          setAutoRotateConfig({
            active: true,
            speed: s,
            delay: d,
            isInteracting: false,
            isWaiting: false,
          });
        });
      },
      stopAutoRotate: () => {
        console.log("停止自动旋转");
        setAutoRotateConfig((prev) => ({ ...prev, active: false }));
      },
      zoomIn: (factor = 1.2) => {
        const f = Math.max(1.01, Number(factor) || 1.2);

        // 执行缩放动作的封装
        const doZoom = (onDone) => {
          const newPos = calculateZoomPos(f, true); // true = zoomIn
          if (newPos) {
            animateCamera(newPos, () => {
              // 关键修改：缩放完成后，更新 initialState，这样后续复位就会回到这个新位置
              if (initialState.current) {
                initialState.current.position.copy(newPos);
                // target 通常不变，但为了保险也同步一下当前 target
                if (controlsRef.current) {
                  initialState.current.target.copy(controlsRef.current.target);
                }
              }
              if (onDone) onDone();
            });
          } else if (onDone) {
            onDone();
          }
        };

        if (autoRotateConfig.active) {
          // 流程：暂停 -> 复位 -> 缩放 -> 等待(handleInteractionEnd)
          handleInteractionStart(); // 1. 暂停
          resetToInitial(() => {
            // 2. 复位到(旧)初始位置
            doZoom(() => {
              // 3. 缩放并更新初始位置
              handleInteractionEnd(); // 4. 进入等待恢复流程 (恢复后旋转将基于新位置)
            });
          });
        } else {
          doZoom(); // 直接缩放
        }
      },
      zoomOut: (factor = 1.2) => {
        const f = Math.max(1.01, Number(factor) || 1.2);

        const doZoom = (onDone) => {
          const newPos = calculateZoomPos(f, false); // false = zoomOut
          if (newPos) {
            animateCamera(newPos, () => {
              // 关键修改：更新 initialState
              if (initialState.current) {
                initialState.current.position.copy(newPos);
                if (controlsRef.current) {
                  initialState.current.target.copy(controlsRef.current.target);
                }
              }
              if (onDone) onDone();
            });
          } else if (onDone) {
            onDone();
          }
        };

        if (autoRotateConfig.active) {
          handleInteractionStart();
          resetToInitial(() => {
            doZoom(() => {
              handleInteractionEnd();
            });
          });
        } else {
          doZoom();
        }
      },
    };
    return () => {
      // 组件卸载时清理全局对象
      if (window.sceneFc) {
        delete window.sceneFc;
      }
    };
  }, [resetToInitial, baseDistance, camera, autoRotateConfig.active, handleInteractionStart, handleInteractionEnd, animateCamera, calculateZoomPos]);

  // 清理定时器
  useEffect(() => {
    return () => {
      if (timerRef.current) clearTimeout(timerRef.current);
    };
  }, []);

  // 双击复位监听 (Native DOM Event)
  useEffect(() => {
    const dom = gl.domElement;
    const handleDblClick = (e) => {
      e.preventDefault();
      resetToInitial();
    };
    dom.addEventListener("dblclick", handleDblClick);
    return () => dom.removeEventListener("dblclick", handleDblClick);
  }, [gl, resetToInitial]);

  return (
    <>
      {/* Bounds: 自动计算模型包围盒并调整相机视角 */}
      {/* 不渲染任何 UI，仅负责逻辑检测 */}
      {/* 
         注意：Bounds 需要渲染其中的 children 才能计算。
      */}
      
      {/* 恢复 Bounds 的渲染，不被 visible 控制，改用 CSS 遮挡或 LoadingUI 延长显示 */}
      <Bounds fit clip margin={1.5}>
          <ModelLoader url={url} fileType={fileType} />
      </Bounds>

      {/* 遮挡层：在 Fit 调整完成前一直显示白色背景，遮住 "先1倍再2倍" 的跳变过程 */}
      {/* <LoadingOverlay visible={!fitAdjusted} color={progressColor} /> */}
      
      <CameraController
        controlsRef={controlsRef}
        zoomMin={zoomMin} zoomMax={zoomMax} baseDistance={baseDistance} autoRotateSpeed={effectiveSpeed} enabled={isReady} onStart={handleInteractionStart} onEnd={handleInteractionEnd} />

      {/* 不渲染任何 UI，仅负责逻辑检测 */}
      <FitWatcher controlsRef={controlsRef} onFitReady={handleFitReady} zoomMin={zoomMin} />
    </>
  );
}

export default function Scene({ blobUrl, zoomMin, zoomMax, progressColor, onModelReady }) {
  // 推断文件类型，传递给 ModelLoader
  const fileType = useMemo(() => {
    if (!blobUrl) return null;
    // 由于 blobUrl 没有后缀，这里需要外部传入 fileType 或者我们假设它是 gltf
    // 之前是根据 url 推断，现在 url 可能是 blob:http://...
    // 但 fetchState.blobUrl 是 blob，原来的 url 是真实的 url
    // 我们需要原来的 url 来推断类型，或者让 App 传进来
    // 简单起见，我们假设外部会处理好，或者我们这里只负责渲染
    // 实际上 SceneContent 需要 fileType
    return "gltf"; // 默认
  }, [blobUrl]);

  // 为了保持兼容，我们还是需要真实的 URL 来推断类型，或者修改 App 传 fileType
  // 这里暂时简化，假设都是 gltf，如果需要支持 fbx，App 需要传 fileType
  
  return (
    <>
      <ambientLight intensity={0.8} />
      <directionalLight position={[5, 5, 5]} intensity={1.0} />
      {blobUrl && (
        <Suspense fallback={null}>
          <SceneContent 
            url={blobUrl} 
            zoomMin={zoomMin} 
            zoomMax={zoomMax} 
            fileType={fileType} 
            onReady={onModelReady}
          />
        </Suspense>
      )}
    </>
  );
}
